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 | |
parent | 74b133ca50b094231721e82ac94448e28b0aa377 (diff) |
remove unnecessary jsx stuff
Diffstat (limited to 'frontend/src/components')
33 files changed, 0 insertions, 4828 deletions
diff --git a/frontend/src/components/App.jsx b/frontend/src/components/App.jsx deleted file mode 100644 index 96083cd..0000000 --- a/frontend/src/components/App.jsx +++ /dev/null @@ -1,70 +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 {BrowserRouter as Router} from "react-router-dom"; -import dispatcher from "../dispatcher"; -import Notifications from "./Notifications"; -import ConfigurationPage from "./pages/ConfigurationPage"; -import MainPage from "./pages/MainPage"; -import ServiceUnavailablePage from "./pages/ServiceUnavailablePage"; - -class App extends Component { - - state = {}; - - componentDidMount() { - dispatcher.register("notifications", this.handleNotifications); - - setInterval(() => { - if (document.title.endsWith("❚")) { - document.title = document.title.slice(0, -1); - } else { - document.title += "❚"; - } - }, 500); - } - - componentWillUnmount() { - dispatcher.unregister(this.handleNotifications); - } - - handleNotifications = (payload) => { - if (payload.event === "connected") { - this.setState({ - connected: true, - configured: payload.message["is_configured"], - version: payload.message["version"] - }); - } - }; - - render() { - return ( - <Router> - <Notifications/> - {this.state.connected ? - (this.state.configured ? <MainPage version={this.state.version}/> : - <ConfigurationPage onConfigured={() => this.setState({configured: true})}/>) : - <ServiceUnavailablePage/> - } - </Router> - ); - } -} - -export default App; diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx deleted file mode 100644 index 4695bd9..0000000 --- a/frontend/src/components/Header.jsx +++ /dev/null @@ -1,99 +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 {Link, withRouter} from "react-router-dom"; -import Typed from "typed.js"; -import {cleanNumber, validatePort} from "../utils"; -import ButtonField from "./fields/ButtonField"; -import AdvancedFilters from "./filters/AdvancedFilters"; -import BooleanConnectionsFilter from "./filters/BooleanConnectionsFilter"; -import ExitSearchFilter from "./filters/ExitSearchFilter"; -import RulesConnectionsFilter from "./filters/RulesConnectionsFilter"; -import StringConnectionsFilter from "./filters/StringConnectionsFilter"; -import "./Header.scss"; - -import classNames from 'classnames'; - -class Header extends Component { - - componentDidMount() { - const options = { - strings: ["caronte$ "], - typeSpeed: 50, - cursorChar: "❚" - }; - this.typed = new Typed(this.el, options); - } - - componentWillUnmount() { - this.typed.destroy(); - } - - render() { - return ( - <header className={classNames("header", {"configured": this.props.configured})}> - <div className="header-content"> - <h1 className="header-title type-wrap"> - <Link to="/"> - <span style={{whiteSpace: "pre"}} ref={(el) => { - this.el = el; - }}/> - </Link> - </h1> - - {this.props.configured && - <div className="filters-bar"> - <StringConnectionsFilter filterName="service_port" - defaultFilterValue="all_ports" - replaceFunc={cleanNumber} - validateFunc={validatePort} - key="service_port_filter" - width={200} small inline/> - <RulesConnectionsFilter/> - <BooleanConnectionsFilter filterName={"marked"}/> - <ExitSearchFilter/> - <AdvancedFilters onClick={this.props.onOpenFilters}/> - </div> - } - - {this.props.configured && - <div className="header-buttons"> - <Link to={"/searches" + this.props.location.search}> - <ButtonField variant="pink" name="searches" bordered/> - </Link> - <Link to={"/pcaps" + this.props.location.search}> - <ButtonField variant="purple" name="pcaps" bordered/> - </Link> - <Link to={"/rules" + this.props.location.search}> - <ButtonField variant="deep-purple" name="rules" bordered/> - </Link> - <Link to={"/services" + this.props.location.search}> - <ButtonField variant="indigo" name="services" bordered/> - </Link> - <Link to={"/stats" + this.props.location.search}> - <ButtonField variant="blue" name="stats" bordered/> - </Link> - </div> - } - </div> - </header> - ); - } -} - -export default withRouter(Header); diff --git a/frontend/src/components/Notifications.jsx b/frontend/src/components/Notifications.jsx deleted file mode 100644 index 5afdb7b..0000000 --- a/frontend/src/components/Notifications.jsx +++ /dev/null @@ -1,147 +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 dispatcher from "../dispatcher"; -import { randomClassName } from "../utils"; -import "./Notifications.scss"; -import _ from "lodash"; -import classNames from "classnames"; - -class Notifications extends Component { - state = { - notifications: [], - closedNotifications: [], - }; - - componentDidMount() { - dispatcher.register("notifications", this.handleNotifications); - } - - componentWillUnmount() { - dispatcher.unregister(this.handleNotifications); - } - - handleNotifications = (n) => this.notificationHandler(n); - - notificationHandler = (n) => { - switch (n.event) { - case "connected": - n.title = "connected"; - n.description = `number of active clients: ${n.message["connected_clients"]}`; - return this.pushNotification(n); - case "services.edit": - n.title = "services updated"; - n.description = `updated "${n.message["name"]}" on port ${n.message["port"]}`; - n.variant = "blue"; - return this.pushNotification(n); - case "rules.new": - n.title = "rules updated"; - n.description = `new rule added: ${n.message["name"]}`; - n.variant = "green"; - return this.pushNotification(n); - case "rules.edit": - n.title = "rules updated"; - n.description = `existing rule updated: ${n.message["name"]}`; - n.variant = "blue"; - return this.pushNotification(n); - case "pcap.completed": - n.title = "new pcap analyzed"; - n.description = `${n.message["processed_packets"]} packets processed`; - n.variant = "blue"; - return this.pushNotification(n); - case "timeline.range.large": - n.title = "timeline cropped"; - n.description = `the maximum range is 24h`; - n.variant = "red"; - return this.pushNotification(n); - default: - return null; - } - }; - - pushNotification = (notification) => { - const notifications = this.state.notifications; - notification.id = randomClassName(); - notifications.push(notification); - this.setState({ notifications }); - setTimeout(() => { - const notifications = this.state.notifications; - notification.open = true; - this.setState({ notifications }); - }, 100); - - const hideHandle = setTimeout(() => { - const notifications = _.without(this.state.notifications, notification); - const closedNotifications = this.state.closedNotifications.concat([ - notification, - ]); - notification.closed = true; - this.setState({ notifications, closedNotifications }); - }, 5000); - - const removeHandle = setTimeout(() => { - const closedNotifications = _.without( - this.state.closedNotifications, - notification - ); - this.setState({ closedNotifications }); - }, 6000); - - notification.onClick = () => { - clearTimeout(hideHandle); - clearTimeout(removeHandle); - const notifications = _.without(this.state.notifications, notification); - this.setState({ notifications }); - }; - }; - - render() { - return ( - <div className="notifications"> - <div className="notifications-list"> - {this.state.closedNotifications - .concat(this.state.notifications) - .map((n) => { - const notificationClassnames = { - notification: true, - "notification-closed": n.closed, - "notification-open": n.open, - }; - if (n.variant) { - notificationClassnames[`notification-${n.variant}`] = true; - } - return ( - <div - key={n.id} - className={classNames(notificationClassnames)} - onClick={n.onClick} - > - <h3 className="notification-title">{n.title}</h3> - <pre className="notification-description"> - {n.description} - </pre> - </div> - ); - })} - </div> - </div> - ); - } -} - -export default Notifications; diff --git a/frontend/src/components/Timeline.jsx b/frontend/src/components/Timeline.jsx deleted file mode 100644 index faaa8de..0000000 --- a/frontend/src/components/Timeline.jsx +++ /dev/null @@ -1,405 +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 { withRouter } from "react-router-dom"; - -import ChartContainer from "react-timeseries-charts/lib/components/ChartContainer"; -import ChartRow from "react-timeseries-charts/lib/components/ChartRow"; -import Charts from "react-timeseries-charts/lib/components/Charts"; -import LineChart from "react-timeseries-charts/lib/components/LineChart"; -import MultiBrush from "react-timeseries-charts/lib/components/MultiBrush"; -import Resizable from "react-timeseries-charts/lib/components/Resizable"; -import YAxis from "react-timeseries-charts/lib/components/YAxis"; -import { TimeRange, TimeSeries } from "pondjs"; -import styler from "react-timeseries-charts/lib/js/styler"; - -import backend from "../backend"; -import dispatcher from "../dispatcher"; -import log from "../log"; -import ChoiceField from "./fields/ChoiceField"; -import "./Timeline.scss"; - -const minutes = 60 * 1000; -const maxTimelineRange = 24 * 60 * minutes; -import classNames from "classnames"; - -const leftSelectionPaddingMultiplier = 24; -const rightSelectionPaddingMultiplier = 8; - -class Timeline extends Component { - state = { - metric: "connections_per_service", - }; - - constructor() { - super(); - - this.disableTimeSeriesChanges = false; - this.selectionTimeout = null; - } - - componentDidMount() { - const urlParams = new URLSearchParams(this.props.location.search); - this.setState({ - servicePortFilter: urlParams.get("service_port") || null, - matchedRulesFilter: urlParams.getAll("matched_rules") || null, - }); - - this.loadStatistics(this.state.metric).then(() => - log.debug("Statistics loaded after mount") - ); - dispatcher.register( - "connections_filters", - this.handleConnectionsFiltersCallback - ); - dispatcher.register("connection_updates", this.handleConnectionUpdates); - dispatcher.register("notifications", this.handleNotifications); - dispatcher.register("pulse_timeline", this.handlePulseTimeline); - } - - componentWillUnmount() { - dispatcher.unregister(this.handleConnectionsFiltersCallback); - dispatcher.unregister(this.handleConnectionUpdates); - dispatcher.unregister(this.handleNotifications); - dispatcher.unregister(this.handlePulseTimeline); - } - - loadStatistics = async (metric) => { - const urlParams = new URLSearchParams(); - urlParams.set("metric", metric); - - let columns = []; - if (metric === "matched_rules") { - let rules = await this.loadRules(); - if (this.state.matchedRulesFilter.length > 0) { - this.state.matchedRulesFilter.forEach((id) => { - urlParams.append("rules_ids", id); - }); - columns = this.state.matchedRulesFilter; - } else { - columns = rules.map((r) => r.id); - } - } else { - let services = await this.loadServices(); - const filteredPort = this.state.servicePortFilter; - if (filteredPort && services[filteredPort]) { - const service = services[filteredPort]; - services = {}; - services[filteredPort] = service; - } - - columns = Object.keys(services); - columns.forEach((port) => urlParams.append("ports", port)); - } - - const metrics = (await backend.get("/api/statistics?" + urlParams)).json; - if (metrics.length === 0) { - return; - } - - const zeroFilledMetrics = []; - const toTime = (m) => new Date(m["range_start"]).getTime(); - - let i; - let timeStart = toTime(metrics[0]) - minutes; - for (i = 0; timeStart < 0 && i < metrics.length; i++) { - // workaround to remove negative timestamps :( - timeStart = toTime(metrics[i]) - minutes; - } - - let timeEnd = toTime(metrics[metrics.length - 1]) + minutes; - if (timeEnd - timeStart > maxTimelineRange) { - timeEnd = timeStart + maxTimelineRange; - - const now = new Date().getTime(); - if ( - !this.lastDisplayNotificationTime || - this.lastDisplayNotificationTime + minutes < now - ) { - this.lastDisplayNotificationTime = now; - dispatcher.dispatch("notifications", { event: "timeline.range.large" }); - } - } - - for (let interval = timeStart; interval <= timeEnd; interval += minutes) { - if (i < metrics.length && interval === toTime(metrics[i])) { - const m = metrics[i++]; - m["range_start"] = new Date(m["range_start"]); - zeroFilledMetrics.push(m); - } else { - const m = {}; - m["range_start"] = new Date(interval); - m[metric] = {}; - columns.forEach((c) => (m[metric][c] = 0)); - zeroFilledMetrics.push(m); - } - } - - const series = new TimeSeries({ - name: "statistics", - columns: ["time"].concat(columns), - points: zeroFilledMetrics.map((m) => - [m["range_start"]].concat( - columns.map((c) => - metric in m && m[metric] != null ? m[metric][c] || 0 : 0 - ) - ) - ), - }); - - const start = series.range().begin(); - const end = series.range().end(); - - this.setState({ - metric, - series, - timeRange: new TimeRange(start, end), - columns, - start, - end, - }); - }; - - loadServices = async () => { - const services = (await backend.get("/api/services")).json; - this.setState({ services }); - return services; - }; - - loadRules = async () => { - const rules = (await backend.get("/api/rules")).json; - this.setState({ rules }); - return rules; - }; - - createStyler = () => { - if (this.state.metric === "matched_rules") { - return styler( - this.state.rules.map((rule) => { - return { key: rule.id, color: rule.color, width: 2 }; - }) - ); - } else { - return styler( - Object.keys(this.state.services).map((port) => { - return { - key: port, - color: this.state.services[port].color, - width: 2, - }; - }) - ); - } - }; - - handleTimeRangeChange = (timeRange) => { - if (!this.disableTimeSeriesChanges) { - this.setState({ timeRange }); - } - }; - - handleSelectionChange = (timeRange) => { - this.disableTimeSeriesChanges = true; - - this.setState({ selection: timeRange }); - if (this.selectionTimeout) { - clearTimeout(this.selectionTimeout); - } - this.selectionTimeout = setTimeout(() => { - dispatcher.dispatch("timeline_updates", { - from: timeRange.begin(), - to: timeRange.end(), - }); - this.selectionTimeout = null; - this.disableTimeSeriesChanges = false; - }, 1000); - }; - - handleConnectionsFiltersCallback = (payload) => { - if ( - "service_port" in payload && - this.state.servicePortFilter !== payload["service_port"] - ) { - this.setState({ servicePortFilter: payload["service_port"] }); - this.loadStatistics(this.state.metric).then(() => - log.debug("Statistics reloaded after service port changed") - ); - } - if ( - "matched_rules" in payload && - this.state.matchedRulesFilter !== payload["matched_rules"] - ) { - this.setState({ matchedRulesFilter: payload["matched_rules"] }); - this.loadStatistics(this.state.metric).then(() => - log.debug("Statistics reloaded after matched rules changed") - ); - } - }; - - handleConnectionUpdates = (payload) => { - if ( - payload.from >= this.state.start && - payload.from < payload.to && - payload.to <= this.state.end - ) { - this.setState({ - selection: new TimeRange(payload.from, payload.to), - }); - this.adjustSelection(); - } - }; - - handleNotifications = (payload) => { - if ( - payload.event === "services.edit" && - this.state.metric !== "matched_rules" - ) { - this.loadStatistics(this.state.metric).then(() => - log.debug("Statistics reloaded after services updates") - ); - } else if ( - payload.event.startsWith("rules") && - this.state.metric === "matched_rules" - ) { - this.loadStatistics(this.state.metric).then(() => - log.debug("Statistics reloaded after rules updates") - ); - } else if (payload.event === "pcap.completed") { - this.loadStatistics(this.state.metric).then(() => - log.debug("Statistics reloaded after pcap processed") - ); - } - }; - - handlePulseTimeline = (payload) => { - this.setState({ pulseTimeline: true }); - setTimeout(() => this.setState({ pulseTimeline: false }), payload.duration); - }; - - adjustSelection = () => { - const seriesRange = this.state.series.range(); - const selection = this.state.selection; - const delta = selection.end() - selection.begin(); - const start = Math.max( - selection.begin().getTime() - delta * leftSelectionPaddingMultiplier, - seriesRange.begin().getTime() - ); - const end = Math.min( - selection.end().getTime() + delta * rightSelectionPaddingMultiplier, - seriesRange.end().getTime() - ); - this.setState({ timeRange: new TimeRange(start, end) }); - }; - - aggregateSeries = (func) => { - const values = this.state.series - .columns() - .map((c) => this.state.series[func](c)); - return Math[func](...values); - }; - - render() { - if (!this.state.series) { - return null; - } - - return ( - <footer className="footer"> - <div - className={classNames("time-line", { - "pulse-timeline": this.state.pulseTimeline, - })} - > - <Resizable> - <ChartContainer - timeRange={this.state.timeRange} - enableDragZoom={false} - paddingTop={5} - minDuration={60000} - maxTime={this.state.end} - minTime={this.state.start} - paddingLeft={0} - paddingRight={0} - paddingBottom={0} - enablePanZoom={true} - utc={false} - onTimeRangeChanged={this.handleTimeRangeChange} - > - <ChartRow height={this.props.height - 70}> - <YAxis - id="axis1" - hideAxisLine - min={this.aggregateSeries("min")} - max={this.aggregateSeries("max")} - width="35" - type="linear" - transition={300} - /> - <Charts> - <LineChart - axis="axis1" - series={this.state.series} - columns={this.state.columns} - style={this.createStyler()} - interpolation="curveBasis" - /> - - <MultiBrush - timeRanges={[this.state.selection]} - allowSelectionClear={false} - allowFreeDrawing={false} - onTimeRangeChanged={this.handleSelectionChange} - /> - </Charts> - </ChartRow> - </ChartContainer> - </Resizable> - - <div className="metric-selection"> - <ChoiceField - inline - small - keys={[ - "connections_per_service", - "client_bytes_per_service", - "server_bytes_per_service", - "duration_per_service", - "matched_rules", - ]} - values={[ - "connections_per_service", - "client_bytes_per_service", - "server_bytes_per_service", - "duration_per_service", - "matched_rules", - ]} - onChange={(metric) => - this.loadStatistics(metric).then(() => - log.debug("Statistics loaded after metric changes") - ) - } - value={this.state.metric} - /> - </div> - </div> - </footer> - ); - } -} - -export default withRouter(Timeline); diff --git a/frontend/src/components/dialogs/CommentDialog.jsx b/frontend/src/components/dialogs/CommentDialog.jsx deleted file mode 100644 index 970aa83..0000000 --- a/frontend/src/components/dialogs/CommentDialog.jsx +++ /dev/null @@ -1,70 +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 {Modal} from "react-bootstrap"; -import backend from "../../backend"; -import log from "../../log"; -import ButtonField from "../fields/ButtonField"; -import TextField from "../fields/TextField"; - -class CommentDialog extends Component { - - state = {}; - - componentDidMount() { - this.setState({comment: this.props.initialComment || ""}); - } - - setComment = () => { - if (this.state.comment === this.props.initialComment) { - return this.close(); - } - const comment = this.state.comment || null; - backend.post(`/api/connections/${this.props.connectionId}/comment`, {comment}) - .then((_) => { - this.close(); - }).catch((e) => { - log.error(e); - this.setState({error: "failed to save comment"}); - }); - }; - - close = () => this.props.onSave(this.state.comment || null); - - render() { - return ( - <Modal show size="md" aria-labelledby="comment-dialog" centered> - <Modal.Header> - <Modal.Title id="comment-dialog"> - ~/.comment - </Modal.Title> - </Modal.Header> - <Modal.Body> - <TextField value={this.state.comment} onChange={(comment) => this.setState({comment})} - rows={7} error={this.state.error}/> - </Modal.Body> - <Modal.Footer className="dialog-footer"> - <ButtonField variant="red" bordered onClick={this.close} name="cancel"/> - <ButtonField variant="green" bordered onClick={this.setComment} name="save"/> - </Modal.Footer> - </Modal> - ); - } -} - -export default CommentDialog; diff --git a/frontend/src/components/dialogs/CopyDialog.jsx b/frontend/src/components/dialogs/CopyDialog.jsx deleted file mode 100644 index 069fd2e..0000000 --- a/frontend/src/components/dialogs/CopyDialog.jsx +++ /dev/null @@ -1,69 +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 {Modal} from "react-bootstrap"; -import ButtonField from "../fields/ButtonField"; -import TextField from "../fields/TextField"; - -class CopyDialog extends Component { - - state = { - copyButtonText: "copy" - }; - - constructor(props) { - super(props); - this.textbox = React.createRef(); - } - - copyActionValue = () => { - this.textbox.current.select(); - document.execCommand("copy"); - this.setState({copyButtonText: "copied!"}); - this.timeoutHandle = setTimeout(() => this.setState({copyButtonText: "copy"}), 3000); - }; - - componentWillUnmount() { - if (this.timeoutHandle) { - clearTimeout(this.timeoutHandle); - } - } - - render() { - return ( - <Modal show={true} size="lg" aria-labelledby="message-action-dialog" centered> - <Modal.Header> - <Modal.Title id="message-action-dialog"> - {this.props.name} - </Modal.Title> - </Modal.Header> - <Modal.Body> - <TextField readonly={this.props.readonly} value={this.props.value} textRef={this.textbox} - rows={15}/> - </Modal.Body> - <Modal.Footer className="dialog-footer"> - <ButtonField variant="red" bordered onClick={this.props.onHide} name="close"/> - <ButtonField variant="green" bordered onClick={this.copyActionValue} - name={this.state.copyButtonText}/> - </Modal.Footer> - </Modal> - ); - } -} - -export default CopyDialog; diff --git a/frontend/src/components/dialogs/Filters.jsx b/frontend/src/components/dialogs/Filters.jsx deleted file mode 100644 index a2407df..0000000 --- a/frontend/src/components/dialogs/Filters.jsx +++ /dev/null @@ -1,85 +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 {Modal} from "react-bootstrap"; -import {cleanNumber, validateIpAddress, validateMin, validatePort} from "../../utils"; -import ButtonField from "../fields/ButtonField"; -import StringConnectionsFilter from "../filters/StringConnectionsFilter"; -import "./Filters.scss"; - -class Filters extends Component { - - render() { - return ( - <Modal - {...this.props} - show={true} - size="lg" - aria-labelledby="filters-dialog" - centered - > - <Modal.Header> - <Modal.Title id="filters-dialog"> - ~/advanced_filters - </Modal.Title> - </Modal.Header> - <Modal.Body> - <div className="advanced-filters d-flex"> - <div className="flex-fill"> - <StringConnectionsFilter filterName="client_address" - defaultFilterValue="all_addresses" - validateFunc={validateIpAddress} - key="client_address_filter"/> - <StringConnectionsFilter filterName="min_duration" - defaultFilterValue="0" - replaceFunc={cleanNumber} - validateFunc={validateMin(0)} - key="min_duration_filter"/> - <StringConnectionsFilter filterName="min_bytes" - defaultFilterValue="0" - replaceFunc={cleanNumber} - validateFunc={validateMin(0)} - key="min_bytes_filter"/> - </div> - - <div className="flex-fill"> - <StringConnectionsFilter filterName="client_port" - defaultFilterValue="all_ports" - replaceFunc={cleanNumber} - validateFunc={validatePort} - key="client_port_filter"/> - <StringConnectionsFilter filterName="max_duration" - defaultFilterValue="∞" - replaceFunc={cleanNumber} - key="max_duration_filter"/> - <StringConnectionsFilter filterName="max_bytes" - defaultFilterValue="∞" - replaceFunc={cleanNumber} - key="max_bytes_filter"/> - </div> - </div> - </Modal.Body> - <Modal.Footer className="dialog-footer"> - <ButtonField variant="red" bordered onClick={this.props.onHide} name="close"/> - </Modal.Footer> - </Modal> - ); - } -} - -export default Filters; diff --git a/frontend/src/components/fields/ButtonField.jsx b/frontend/src/components/fields/ButtonField.jsx deleted file mode 100644 index ae5b33a..0000000 --- a/frontend/src/components/fields/ButtonField.jsx +++ /dev/null @@ -1,78 +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 "./ButtonField.scss"; -import "./common.scss"; -import classNames from "classnames"; - -class ButtonField extends Component { - render() { - const handler = () => { - if (typeof this.props.onClick === "function") { - this.props.onClick(); - } - }; - - let buttonClassnames = { - "button-bordered": this.props.bordered, - }; - if (this.props.variant) { - buttonClassnames[`button-variant-${this.props.variant}`] = true; - } - - let buttonStyle = {}; - if (this.props.color) { - buttonStyle["backgroundColor"] = this.props.color; - } - if (this.props.border) { - buttonStyle["borderColor"] = this.props.border; - } - if (this.props.fullSpan) { - buttonStyle["width"] = "100%"; - } - if (this.props.rounded) { - buttonStyle["borderRadius"] = "3px"; - } - if (this.props.inline) { - buttonStyle["marginTop"] = "8px"; - } - - return ( - <div - className={classNames( - "field", - "button-field", - { "field-small": this.props.small }, - { "field-active": this.props.active } - )} - > - <button - type="button" - className={classNames(buttonClassnames)} - onClick={handler} - style={buttonStyle} - disabled={this.props.disabled} - > - {this.props.name} - </button> - </div> - ); - } -} - -export default ButtonField; diff --git a/frontend/src/components/fields/CheckField.jsx b/frontend/src/components/fields/CheckField.jsx deleted file mode 100644 index 1a04f18..0000000 --- a/frontend/src/components/fields/CheckField.jsx +++ /dev/null @@ -1,67 +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 { randomClassName } from "../../utils"; -import "./CheckField.scss"; -import "./common.scss"; - -import classNames from "classnames"; - -class CheckField extends Component { - constructor(props) { - super(props); - - this.id = `field-${this.props.name || "noname"}-${randomClassName()}`; - } - - render() { - const checked = this.props.checked || false; - const small = this.props.small || false; - const name = this.props.name || null; - const handler = () => { - if (!this.props.readonly && this.props.onChange) { - this.props.onChange(!checked); - } - }; - - return ( - <div - className={classNames( - "field", - "check-field", - { "field-checked": checked }, - { "field-small": small } - )} - > - <div className="field-input"> - <input - type="checkbox" - id={this.id} - checked={checked} - onChange={handler} - /> - <label htmlFor={this.id}> - {(checked ? "✓ " : "✗ ") + (name != null ? name : "")} - </label> - </div> - </div> - ); - } -} - -export default CheckField; diff --git a/frontend/src/components/fields/ChoiceField.jsx b/frontend/src/components/fields/ChoiceField.jsx deleted file mode 100644 index c44d0a9..0000000 --- a/frontend/src/components/fields/ChoiceField.jsx +++ /dev/null @@ -1,85 +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 {randomClassName} from "../../utils"; -import "./ChoiceField.scss"; -import "./common.scss"; - -import classNames from 'classnames'; - -class ChoiceField extends Component { - - constructor(props) { - super(props); - - this.state = { - expanded: false - }; - - this.id = `field-${this.props.name || "noname"}-${randomClassName()}`; - } - - render() { - const name = this.props.name || null; - const inline = this.props.inline; - - const collapse = () => this.setState({expanded: false}); - const expand = () => this.setState({expanded: true}); - - const handler = (key) => { - collapse(); - if (this.props.onChange) { - this.props.onChange(key); - } - }; - - const keys = this.props.keys || []; - const values = this.props.values || []; - - const options = keys.map((key, i) => - <span className="field-option" key={key} onClick={() => handler(key)}>{values[i]}</span> - ); - - let fieldValue = ""; - if (inline && name) { - fieldValue = name; - } - if (!this.props.onlyName && inline && name) { - fieldValue += ": "; - } - if (!this.props.onlyName) { - fieldValue += this.props.value || "select a value"; - } - - return ( - <div className={classNames("field", "choice-field", {"field-inline": inline}, - {"field-small": this.props.small})}> - {!inline && name && <label className="field-name">{name}:</label>} - <div className={classNames("field-select", {"select-expanded": this.state.expanded})} - tabIndex={0} onBlur={collapse} onClick={() => this.state.expanded ? collapse() : expand()}> - <div className="field-value">{fieldValue}</div> - <div className="field-options"> - {options} - </div> - </div> - </div> - ); - } -} - -export default ChoiceField; diff --git a/frontend/src/components/fields/InputField.jsx b/frontend/src/components/fields/InputField.jsx deleted file mode 100644 index f9609df..0000000 --- a/frontend/src/components/fields/InputField.jsx +++ /dev/null @@ -1,95 +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 {randomClassName} from "../../utils"; -import "./common.scss"; -import "./InputField.scss"; - -import classNames from 'classnames'; - -class InputField extends Component { - - constructor(props) { - super(props); - - this.id = `field-${this.props.name || "noname"}-${randomClassName()}`; - } - - render() { - const active = this.props.active || false; - const invalid = this.props.invalid || false; - const small = this.props.small || false; - const inline = this.props.inline || false; - const name = this.props.name || null; - const value = this.props.value || ""; - const defaultValue = this.props.defaultValue || ""; - const type = this.props.type || "text"; - const error = this.props.error || null; - - const handler = (e) => { - if (typeof this.props.onChange === "function") { - if (type === "file") { - let file = e.target.files[0]; - this.props.onChange(file); - } else if (e == null) { - this.props.onChange(defaultValue); - } else { - this.props.onChange(e.target.value); - } - } - }; - let inputProps = {}; - if (type !== "file") { - inputProps["value"] = value || defaultValue; - } - - return ( - <div className={classNames("field", "input-field", {"field-active": active}, - {"field-invalid": invalid}, {"field-small": small}, {"field-inline": inline})}> - <div className="field-wrapper"> - {name && - <div className="field-name"> - <label>{name}:</label> - </div> - } - <div className="field-input"> - <div className="field-value"> - {type === "file" && <label for={this.id} className={"file-label"}> - {value.name || this.props.placeholder}</label>} - <input type={type} placeholder={this.props.placeholder} id={this.id} - aria-describedby={this.id} onChange={handler} {...inputProps} - readOnly={this.props.readonly}/> - </div> - {type !== "file" && value !== "" && !this.props.readonly && - <div className="field-clear"> - <span onClick={() => handler(null)}>del</span> - </div> - } - </div> - </div> - {error && - <div className="field-error"> - error: {error} - </div> - } - </div> - ); - } -} - -export default InputField; diff --git a/frontend/src/components/fields/TagField.jsx b/frontend/src/components/fields/TagField.jsx deleted file mode 100644 index 79e2314..0000000 --- a/frontend/src/components/fields/TagField.jsx +++ /dev/null @@ -1,75 +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 ReactTags from "react-tag-autocomplete"; -import {randomClassName} from "../../utils"; -import "./common.scss"; -import "./TagField.scss"; - -import classNames from 'classnames'; -import _ from 'lodash'; - -class TagField extends Component { - - state = {}; - - constructor(props) { - super(props); - - this.id = `field-${this.props.name || "noname"}-${randomClassName()}`; - } - - onAddition = (tag) => { - if (typeof this.props.onChange === "function") { - this.props.onChange([].concat(this.props.tags, tag), true, tag); // true == addition - } - }; - - onDelete = (i) => { - if (typeof this.props.onChange === "function") { - const tags = _.clone(this.props.tags); - const tag = tags[i]; - tags.splice(i, 1); - this.props.onChange(tags, true, tag); // false == delete - } - }; - - - render() { - const small = this.props.small || false; - const name = this.props.name || null; - - return ( - <div className={classNames("field", "tag-field", {"field-small": small}, - {"field-inline": this.props.inline})}> - {name && - <div className="field-name"> - <label>{name}:</label> - </div> - } - <div className="field-input"> - <ReactTags {...this.props} tags={this.props.tags || []} autoresize={false} - onDelete={this.onDelete} onAddition={this.onAddition} - placeholderText={this.props.placeholder || ""}/> - </div> - </div> - ); - } -} - -export default TagField; diff --git a/frontend/src/components/fields/TextField.jsx b/frontend/src/components/fields/TextField.jsx deleted file mode 100644 index 1138ffc..0000000 --- a/frontend/src/components/fields/TextField.jsx +++ /dev/null @@ -1,60 +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 {randomClassName} from "../../utils"; -import "./common.scss"; -import "./TextField.scss"; - -import classNames from 'classnames'; - -class TextField extends Component { - - constructor(props) { - super(props); - - this.id = `field-${this.props.name || "noname"}-${randomClassName()}`; - } - - render() { - const name = this.props.name || null; - const error = this.props.error || null; - const rows = this.props.rows || 3; - - const handler = (e) => { - if (this.props.onChange) { - if (e == null) { - this.props.onChange(""); - } else { - this.props.onChange(e.target.value); - } - } - }; - - return ( - <div className={classNames("field", "text-field", {"field-active": this.props.active}, - {"field-invalid": this.props.invalid}, {"field-small": this.props.small})}> - {name && <label htmlFor={this.id}>{name}:</label>} - <textarea id={this.id} placeholder={this.props.defaultValue} onChange={handler} rows={rows} - readOnly={this.props.readonly} value={this.props.value} ref={this.props.textRef}/> - {error && <div className="field-error">error: {error}</div>} - </div> - ); - } -} - -export default TextField; diff --git a/frontend/src/components/filters/AdvancedFilters.jsx b/frontend/src/components/filters/AdvancedFilters.jsx deleted file mode 100644 index 8598185..0000000 --- a/frontend/src/components/filters/AdvancedFilters.jsx +++ /dev/null @@ -1,54 +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 {withRouter} from "react-router-dom"; -import dispatcher from "../../dispatcher"; -import {updateParams} from "../../utils"; -import ButtonField from "../fields/ButtonField"; - -class AdvancedFilters extends Component { - - state = {}; - - componentDidMount() { - this.urlParams = new URLSearchParams(this.props.location.search); - - this.connectionsFiltersCallback = (payload) => { - this.urlParams = updateParams(this.urlParams, payload); - const active = ["client_address", "client_port", "min_duration", "max_duration", "min_bytes", "max_bytes"] - .some((f) => this.urlParams.has(f)); - if (this.state.active !== active) { - this.setState({active}); - } - }; - dispatcher.register("connections_filters", this.connectionsFiltersCallback); - } - - componentWillUnmount() { - dispatcher.unregister(this.connectionsFiltersCallback); - } - - render() { - return ( - <ButtonField onClick={this.props.onClick} name="advanced_filters" small active={this.state.active}/> - ); - } - -} - -export default withRouter(AdvancedFilters); diff --git a/frontend/src/components/filters/BooleanConnectionsFilter.jsx b/frontend/src/components/filters/BooleanConnectionsFilter.jsx deleted file mode 100644 index 0355167..0000000 --- a/frontend/src/components/filters/BooleanConnectionsFilter.jsx +++ /dev/null @@ -1,69 +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 {withRouter} from "react-router-dom"; -import dispatcher from "../../dispatcher"; -import CheckField from "../fields/CheckField"; - -class BooleanConnectionsFilter extends Component { - - state = { - filterActive: "false" - }; - - componentDidMount() { - let params = new URLSearchParams(this.props.location.search); - this.setState({filterActive: this.toBoolean(params.get(this.props.filterName)).toString()}); - - this.connectionsFiltersCallback = (payload) => { - const name = this.props.filterName; - if (name in payload && this.state.filterActive !== payload[name]) { - this.setState({filterActive: payload[name]}); - } - }; - dispatcher.register("connections_filters", this.connectionsFiltersCallback); - } - - componentWillUnmount() { - dispatcher.unregister(this.connectionsFiltersCallback); - } - - toBoolean = (value) => { - return value !== null && value.toLowerCase() === "true"; - }; - - filterChanged = () => { - const newValue = (!this.toBoolean(this.state.filterActive)).toString(); - const urlParams = {}; - urlParams[this.props.filterName] = newValue === "true" ? "true" : null; - dispatcher.dispatch("connections_filters", urlParams); - this.setState({filterActive: newValue}); - }; - - render() { - return ( - <div className="filter" style={{"width": `${this.props.width}px`}}> - <CheckField checked={this.toBoolean(this.state.filterActive)} name={this.props.filterName} - onChange={this.filterChanged} small/> - </div> - ); - } - -} - -export default withRouter(BooleanConnectionsFilter); diff --git a/frontend/src/components/filters/ExitSearchFilter.jsx b/frontend/src/components/filters/ExitSearchFilter.jsx deleted file mode 100644 index 0aacfd6..0000000 --- a/frontend/src/components/filters/ExitSearchFilter.jsx +++ /dev/null @@ -1,57 +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 {withRouter} from "react-router-dom"; -import dispatcher from "../../dispatcher"; -import CheckField from "../fields/CheckField"; - -class ExitSearchFilter extends Component { - - state = {}; - - componentDidMount() { - let params = new URLSearchParams(this.props.location.search); - this.setState({performedSearch: params.get("performed_search")}); - - this.connectionsFiltersCallback = (payload) => { - if (this.state.performedSearch !== payload["performed_search"]) { - this.setState({performedSearch: payload["performed_search"]}); - } - }; - dispatcher.register("connections_filters", this.connectionsFiltersCallback); - } - - componentWillUnmount() { - dispatcher.unregister(this.connectionsFiltersCallback); - } - - render() { - return ( - <> - {this.state.performedSearch && - <div className="filter" style={{"width": `${this.props.width}px`}}> - <CheckField checked={true} name="exit_search" onChange={() => - dispatcher.dispatch("connections_filters", {"performed_search": null})} small/> - </div>} - </> - ); - } - -} - -export default withRouter(ExitSearchFilter); diff --git a/frontend/src/components/filters/RulesConnectionsFilter.jsx b/frontend/src/components/filters/RulesConnectionsFilter.jsx deleted file mode 100644 index 4ae769a..0000000 --- a/frontend/src/components/filters/RulesConnectionsFilter.jsx +++ /dev/null @@ -1,83 +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 {withRouter} from "react-router-dom"; -import backend from "../../backend"; -import dispatcher from "../../dispatcher"; -import TagField from "../fields/TagField"; - -import classNames from 'classnames'; -import _ from 'lodash'; - -class RulesConnectionsFilter extends Component { - - state = { - rules: [], - activeRules: [] - }; - - componentDidMount() { - const params = new URLSearchParams(this.props.location.search); - let activeRules = params.getAll("matched_rules") || []; - - backend.get("/api/rules").then((res) => { - let rules = res.json.flatMap((rule) => rule.enabled ? [{id: rule.id, name: rule.name}] : []); - activeRules = rules.filter((rule) => activeRules.some((id) => rule.id === id)); - this.setState({rules, activeRules}); - }); - - this.connectionsFiltersCallback = (payload) => { - if ("matched_rules" in payload && !_.isEqual(payload["matched_rules"].sort(), this.state.activeRules.sort())) { - const newRules = this.state.rules.filter((r) => payload["matched_rules"].includes(r.id)); - this.setState({ - activeRules: newRules.map((r) => { - return {id: r.id, name: r.name}; - }) - }); - } - }; - dispatcher.register("connections_filters", this.connectionsFiltersCallback); - } - - componentWillUnmount() { - dispatcher.unregister(this.connectionsFiltersCallback); - } - - onChange = (activeRules) => { - if (!_.isEqual(activeRules.sort(), this.state.activeRules.sort())) { - this.setState({activeRules}); - dispatcher.dispatch("connections_filters", {"matched_rules": activeRules.map((r) => r.id)}); - } - }; - - render() { - return ( - <div - className={classNames("filter", "d-inline-block", {"filter-active": this.state.filterActive === "true"})}> - <div className="filter-rules"> - <TagField tags={this.state.activeRules} onChange={this.onChange} - suggestions={_.differenceWith(this.state.rules, this.state.activeRules, _.isEqual)} - minQueryLength={0} name="matched_rules" inline small placeholder="rule_name"/> - </div> - </div> - ); - } - -} - -export default withRouter(RulesConnectionsFilter); diff --git a/frontend/src/components/filters/StringConnectionsFilter.jsx b/frontend/src/components/filters/StringConnectionsFilter.jsx deleted file mode 100644 index c5d7075..0000000 --- a/frontend/src/components/filters/StringConnectionsFilter.jsx +++ /dev/null @@ -1,127 +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 {withRouter} from "react-router-dom"; -import dispatcher from "../../dispatcher"; -import InputField from "../fields/InputField"; - -class StringConnectionsFilter extends Component { - - state = { - fieldValue: "", - filterValue: null, - timeoutHandle: null, - invalidValue: false - }; - - componentDidMount() { - let params = new URLSearchParams(this.props.location.search); - this.updateStateFromFilterValue(params.get(this.props.filterName)); - - this.connectionsFiltersCallback = (payload) => { - const name = this.props.filterName; - if (name in payload && this.state.filterValue !== payload[name]) { - this.updateStateFromFilterValue(payload[name]); - } - }; - dispatcher.register("connections_filters", this.connectionsFiltersCallback); - } - - componentWillUnmount() { - dispatcher.unregister(this.connectionsFiltersCallback); - } - - updateStateFromFilterValue = (filterValue) => { - if (filterValue !== null) { - let fieldValue = filterValue; - if (typeof this.props.decodeFunc === "function") { - fieldValue = this.props.decodeFunc(filterValue); - } - if (typeof this.props.replaceFunc === "function") { - fieldValue = this.props.replaceFunc(fieldValue); - } - if (this.isValueValid(fieldValue)) { - this.setState({fieldValue, filterValue}); - } else { - this.setState({fieldValue, invalidValue: true}); - } - } else { - this.setState({fieldValue: "", filterValue: null}); - } - }; - - isValueValid = (value) => { - return typeof this.props.validateFunc !== "function" || - (typeof this.props.validateFunc === "function" && this.props.validateFunc(value)); - }; - - changeFilterValue = (value) => { - const urlParams = {}; - urlParams[this.props.filterName] = value; - dispatcher.dispatch("connections_filters", urlParams); - }; - - filterChanged = (fieldValue) => { - if (this.state.timeoutHandle) { - clearTimeout(this.state.timeoutHandle); - } - - if (typeof this.props.replaceFunc === "function") { - fieldValue = this.props.replaceFunc(fieldValue); - } - - if (fieldValue === "") { - this.setState({fieldValue: "", filterValue: null, invalidValue: false}); - return this.changeFilterValue(null); - } - - - if (this.isValueValid(fieldValue)) { - let filterValue = fieldValue; - if (filterValue !== "" && typeof this.props.encodeFunc === "function") { - filterValue = this.props.encodeFunc(filterValue); - } - - this.setState({ - fieldValue, - timeoutHandle: setTimeout(() => { - this.setState({filterValue}); - this.changeFilterValue(filterValue); - }, 500), - invalidValue: false - }); - } else { - this.setState({fieldValue, invalidValue: true}); - } - }; - - render() { - let active = this.state.filterValue !== null; - - return ( - <div className="filter" style={{"width": `${this.props.width}px`}}> - <InputField active={active} invalid={this.state.invalidValue} name={this.props.filterName} - placeholder={this.props.defaultFilterValue} onChange={this.filterChanged} - value={this.state.fieldValue} inline={this.props.inline} small={this.props.small}/> - </div> - ); - } - -} - -export default withRouter(StringConnectionsFilter); diff --git a/frontend/src/components/objects/Connection.jsx b/frontend/src/components/objects/Connection.jsx deleted file mode 100644 index 113ed21..0000000 --- a/frontend/src/components/objects/Connection.jsx +++ /dev/null @@ -1,116 +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 backend from "../../backend"; -import dispatcher from "../../dispatcher"; -import {dateTimeToTime, durationBetween, formatSize} from "../../utils"; -import CommentDialog from "../dialogs/CommentDialog"; -import ButtonField from "../fields/ButtonField"; -import TextField from "../fields/TextField"; -import "./Connection.scss"; -import CopyLinkPopover from "./CopyLinkPopover"; -import LinkPopover from "./LinkPopover"; - -import classNames from 'classnames'; - -class Connection extends Component { - - state = { - update: false - }; - - handleAction = (name, comment) => { - if (name === "mark") { - const marked = this.props.data.marked; - backend.post(`/api/connections/${this.props.data.id}/${marked ? "unmark" : "mark"}`) - .then((_) => { - this.props.onMarked(!marked); - this.setState({update: true}); - }); - } else if (name === "comment") { - this.props.onCommented(comment); - this.setState({showCommentDialog: false}); - } - }; - - render() { - let conn = this.props.data; - let serviceName = "/dev/null"; - let serviceColor = "#0f192e"; - if (this.props.services[conn["port_dst"]]) { - const service = this.props.services[conn["port_dst"]]; - serviceName = service.name; - serviceColor = service.color; - } - let startedAt = new Date(conn["started_at"]); - let closedAt = new Date(conn["closed_at"]); - let processedAt = new Date(conn["processed_at"]); - let timeInfo = <div> - <span>Started at {startedAt.toLocaleDateString() + " " + startedAt.toLocaleTimeString()}</span><br/> - <span>Processed at {processedAt.toLocaleDateString() + " " + processedAt.toLocaleTimeString()}</span><br/> - <span>Closed at {closedAt.toLocaleDateString() + " " + closedAt.toLocaleTimeString()}</span> - </div>; - - const commentPopoverContent = <div style={{"width": "250px"}}> - <span>Click to <strong>{conn.comment ? "edit" : "add"}</strong> comment</span> - {conn.comment && <TextField rows={3} value={conn.comment} readonly/>} - </div>; - - return ( - <tr className={classNames("connection", {"connection-selected": this.props.selected}, - {"has-matched-rules": conn.matched_rules.length > 0})}> - <td> - <span className="connection-service"> - <ButtonField small fullSpan color={serviceColor} name={serviceName} - onClick={() => dispatcher.dispatch("connections_filters", - {"service_port": conn["port_dst"].toString()})}/> - </span> - </td> - <td className="clickable" onClick={this.props.onSelected}>{conn["ip_src"]}</td> - <td className="clickable" onClick={this.props.onSelected}>{conn["port_src"]}</td> - <td className="clickable" onClick={this.props.onSelected}>{conn["ip_dst"]}</td> - <td className="clickable" onClick={this.props.onSelected}>{conn["port_dst"]}</td> - <td className="clickable" onClick={this.props.onSelected}> - <LinkPopover text={dateTimeToTime(conn["started_at"])} content={timeInfo} placement="right"/> - </td> - <td className="clickable" onClick={this.props.onSelected}>{durationBetween(startedAt, closedAt)}</td> - <td className="clickable" onClick={this.props.onSelected}>{formatSize(conn["client_bytes"])}</td> - <td className="clickable" onClick={this.props.onSelected}>{formatSize(conn["server_bytes"])}</td> - <td className="connection-actions"> - <LinkPopover text={<span className={classNames("connection-icon", {"icon-enabled": conn.marked})} - onClick={() => this.handleAction("mark")}>!!</span>} - content={<span>Mark this connection</span>} placement="right"/> - <LinkPopover text={<span className={classNames("connection-icon", {"icon-enabled": conn.comment})} - onClick={() => this.setState({showCommentDialog: true})}>@</span>} - content={commentPopoverContent} placement="right"/> - <CopyLinkPopover text="#" value={conn.id} - textClassName={classNames("connection-icon", {"icon-enabled": conn.hidden})}/> - { - this.state.showCommentDialog && - <CommentDialog onSave={(comment) => this.handleAction("comment", comment)} - initialComment={conn.comment} connectionId={conn.id}/> - } - - </td> - </tr> - ); - } - -} - -export default Connection; diff --git a/frontend/src/components/objects/ConnectionMatchedRules.jsx b/frontend/src/components/objects/ConnectionMatchedRules.jsx deleted file mode 100644 index a69cad8..0000000 --- a/frontend/src/components/objects/ConnectionMatchedRules.jsx +++ /dev/null @@ -1,51 +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 {withRouter} from "react-router-dom"; -import dispatcher from "../../dispatcher"; -import ButtonField from "../fields/ButtonField"; -import "./ConnectionMatchedRules.scss"; - -class ConnectionMatchedRules extends Component { - - onMatchedRulesSelected = (id) => { - const params = new URLSearchParams(this.props.location.search); - const rules = params.getAll("matched_rules"); - if (!rules.includes(id)) { - rules.push(id); - dispatcher.dispatch("connections_filters", {"matched_rules": rules}); - } - }; - - render() { - const matchedRules = this.props.matchedRules.map((mr) => { - const rule = this.props.rules.find((r) => r.id === mr); - return <ButtonField key={mr} onClick={() => this.onMatchedRulesSelected(rule.id)} name={rule.name} - color={rule.color} small/>; - }); - - return ( - <tr className="connection-matches"> - <td className="row-label">matched_rules:</td> - <td className="rule-buttons" colSpan={9}>{matchedRules}</td> - </tr> - ); - } -} - -export default withRouter(ConnectionMatchedRules); diff --git a/frontend/src/components/objects/CopyLinkPopover.jsx b/frontend/src/components/objects/CopyLinkPopover.jsx deleted file mode 100644 index b951603..0000000 --- a/frontend/src/components/objects/CopyLinkPopover.jsx +++ /dev/null @@ -1,54 +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 TextField from "../fields/TextField"; -import LinkPopover from "./LinkPopover"; - -class CopyLinkPopover extends Component { - - state = {}; - - constructor(props) { - super(props); - - this.copyTextarea = React.createRef(); - } - - handleClick = () => { - this.copyTextarea.current.select(); - document.execCommand("copy"); - this.setState({copiedMessage: true}); - setTimeout(() => this.setState({copiedMessage: false}), 3000); - }; - - render() { - const copyPopoverContent = <div style={{"width": "250px"}}> - {this.state.copiedMessage ? <span><strong>Copied!</strong></span> : - <span>Click to <strong>copy</strong></span>} - <TextField readonly rows={2} value={this.props.value} textRef={this.copyTextarea}/> - </div>; - - return ( - <LinkPopover text={<span className={this.props.textClassName} - onClick={this.handleClick}>{this.props.text}</span>} - content={copyPopoverContent} placement="right"/> - ); - } -} - -export default CopyLinkPopover; diff --git a/frontend/src/components/objects/LinkPopover.jsx b/frontend/src/components/objects/LinkPopover.jsx deleted file mode 100644 index 551a819..0000000 --- a/frontend/src/components/objects/LinkPopover.jsx +++ /dev/null @@ -1,51 +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 {OverlayTrigger, Popover} from "react-bootstrap"; -import {randomClassName} from "../../utils"; -import "./LinkPopover.scss"; - -class LinkPopover extends Component { - - constructor(props) { - super(props); - - this.id = `link-overlay-${randomClassName()}`; - } - - render() { - const popover = ( - <Popover id={this.id}> - {this.props.title && <Popover.Title as="h3">{this.props.title}</Popover.Title>} - <Popover.Content> - {this.props.content} - </Popover.Content> - </Popover> - ); - - return (this.props.content ? - <OverlayTrigger trigger={["hover", "focus"]} placement={this.props.placement || "top"} - overlay={popover}> - <span className="link-popover">{this.props.text}</span> - </OverlayTrigger> : - <span className="link-popover-empty">{this.props.text}</span> - ); - } -} - -export default LinkPopover; diff --git a/frontend/src/components/pages/ConfigurationPage.jsx b/frontend/src/components/pages/ConfigurationPage.jsx deleted file mode 100644 index c8646fb..0000000 --- a/frontend/src/components/pages/ConfigurationPage.jsx +++ /dev/null @@ -1,183 +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 {createCurlCommand} from "../../utils"; -import validation from "../../validation"; -import ButtonField from "../fields/ButtonField"; -import CheckField from "../fields/CheckField"; -import InputField from "../fields/InputField"; -import TextField from "../fields/TextField"; -import Header from "../Header"; -import LinkPopover from "../objects/LinkPopover"; -import "../panels/common.scss"; -import "./ConfigurationPage.scss"; - -class ConfigurationPage extends Component { - - constructor(props) { - super(props); - this.state = { - settings: { - "config": { - "server_address": "", - "flag_regex": "", - "auth_required": false - }, - "accounts": {} - }, - newUsername: "", - newPassword: "" - }; - } - - saveSettings = () => { - if (this.validateSettings(this.state.settings)) { - backend.post("/setup", this.state.settings).then((_) => { - this.props.onConfigured(); - }).catch((res) => { - this.setState({setupStatusCode: res.status, setupResponse: JSON.stringify(res.json)}); - }); - } - }; - - validateSettings = (settings) => { - let valid = true; - if (!validation.isValidAddress(settings.config["server_address"], true)) { - this.setState({serverAddressError: "invalid ip_address"}); - valid = false; - } - if (settings.config["flag_regex"].length < 8) { - this.setState({flagRegexError: "flag_regex.length < 8"}); - valid = false; - } - - return valid; - }; - - updateParam = (callback) => { - callback(this.state.settings); - this.setState({settings: this.state.settings}); - }; - - addAccount = () => { - if (this.state.newUsername.length !== 0 && this.state.newPassword.length !== 0) { - const settings = this.state.settings; - settings.accounts[this.state.newUsername] = this.state.newPassword; - - this.setState({ - newUsername: "", - newPassword: "", - settings - }); - } else { - this.setState({ - newUsernameActive: this.state.newUsername.length === 0, - newPasswordActive: this.state.newPassword.length === 0 - }); - } - }; - - render() { - const settings = this.state.settings; - const curlCommand = createCurlCommand("/setup", "POST", settings); - - const accounts = Object.entries(settings.accounts).map(([username, password]) => - <tr key={username}> - <td>{username}</td> - <td><LinkPopover text="******" content={password}/></td> - <td><ButtonField variant="red" small rounded name="delete" - onClick={() => this.updateParam((s) => delete s.accounts[username])}/></td> - </tr>).concat(<tr key={"new_account"}> - <td><InputField value={this.state.newUsername} small active={this.state.newUsernameActive} - onChange={(v) => this.setState({newUsername: v})}/></td> - <td><InputField value={this.state.newPassword} small active={this.state.newPasswordActive} - onChange={(v) => this.setState({newPassword: v})}/></td> - <td><ButtonField variant="green" small rounded name="add" onClick={this.addAccount}/></td> - </tr>); - - return ( - <div className="page configuration-page"> - <div className="page-header"> - <Header /> - </div> - - <div className="page-content"> - <div className="pane-container configuration-pane"> - <div className="pane-section"> - <div className="section-header"> - <span className="api-request">POST /setup</span> - <span className="api-response"><LinkPopover text={this.state.setupStatusCode} - content={this.state.setupResponse} - placement="left"/></span> - </div> - - <div className="section-content"> - <Container className="p-0"> - <Row> - <Col> - <InputField name="server_address" value={settings.config["server_address"]} - error={this.state.serverAddressError} - onChange={(v) => this.updateParam((s) => s.config["server_address"] = v)}/> - <InputField name="flag_regex" value={settings.config["flag_regex"]} - onChange={(v) => this.updateParam((s) => s.config["flag_regex"] = v)} - error={this.state.flagRegexError}/> - <div style={{"marginTop": "10px"}}> - <CheckField checked={settings.config["auth_required"]} name="auth_required" - onChange={(v) => this.updateParam((s) => s.config["auth_required"] = v)}/> - </div> - - </Col> - - <Col> - accounts: - <div className="section-table"> - <Table borderless size="sm"> - <thead> - <tr> - <th>username</th> - <th>password</th> - <th>actions</th> - </tr> - </thead> - <tbody> - {accounts} - </tbody> - </Table> - </div> - </Col> - </Row> - </Container> - - <TextField value={curlCommand} rows={4} readonly small={true}/> - </div> - - <div className="section-footer"> - <ButtonField variant="green" name="save" bordered onClick={this.saveSettings}/> - </div> - </div> - </div> - </div> - </div> - ); - } -} - -export default ConfigurationPage; diff --git a/frontend/src/components/pages/MainPage.jsx b/frontend/src/components/pages/MainPage.jsx deleted file mode 100644 index b9a9e6e..0000000 --- a/frontend/src/components/pages/MainPage.jsx +++ /dev/null @@ -1,97 +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 {ReflexContainer, ReflexElement, ReflexSplitter} from "react-reflex"; -import "react-reflex/styles.css" -import {Route, Switch} from "react-router-dom"; -import Filters from "../dialogs/Filters"; -import Header from "../Header"; -import Connections from "../panels/ConnectionsPane"; -import MainPane from "../panels/MainPane"; -import PcapsPane from "../panels/PcapsPane"; -import RulesPane from "../panels/RulesPane"; -import SearchPane from "../panels/SearchPane"; -import ServicesPane from "../panels/ServicesPane"; -import StatsPane from "../panels/StatsPane"; -import StreamsPane from "../panels/StreamsPane"; -import "./MainPage.scss"; - -class MainPage extends Component { - - state = { - timelineHeight: 210 - }; - - handleTimelineResize = (e) => { - if (this.timelineTimeoutHandle) { - clearTimeout(this.timelineTimeoutHandle); - } - - this.timelineTimeoutHandle = setTimeout(() => - this.setState({timelineHeight: e.domElement.clientHeight}), 100); - }; - - render() { - let modal; - if (this.state.filterWindowOpen) { - modal = <Filters onHide={() => this.setState({filterWindowOpen: false})}/>; - } - - return ( - <ReflexContainer orientation="horizontal" className="page main-page"> - <div className="fuck-css"> - <ReflexElement className="page-header"> - <Header onOpenFilters={() => this.setState({filterWindowOpen: true})} configured={true}/> - {modal} - </ReflexElement> - </div> - - <ReflexElement className="page-content" flex={1}> - <ReflexContainer orientation="vertical" className="page-content"> - <ReflexElement className="pane connections-pane"> - <Connections onSelected={(c) => this.setState({selectedConnection: c})}/> - </ReflexElement> - - <ReflexSplitter/> - - <ReflexElement className="pane details-pane"> - <Switch> - <Route path="/searches" children={<SearchPane/>}/> - <Route path="/pcaps" children={<PcapsPane/>}/> - <Route path="/rules" children={<RulesPane/>}/> - <Route path="/services" children={<ServicesPane/>}/> - <Route path="/stats" children={<StatsPane/>}/> - <Route exact path="/connections/:id" - children={<StreamsPane connection={this.state.selectedConnection}/>}/> - <Route children={<MainPane version={this.props.version}/>}/> - </Switch> - </ReflexElement> - </ReflexContainer> - </ReflexElement> - - <ReflexSplitter propagate={true}/> - - <ReflexElement className="page-footer" onResize={this.handleTimelineResize}> - - </ReflexElement> - </ReflexContainer> - ); - } -} - -export default MainPage; diff --git a/frontend/src/components/pages/ServiceUnavailablePage.jsx b/frontend/src/components/pages/ServiceUnavailablePage.jsx deleted file mode 100644 index deb4cf8..0000000 --- a/frontend/src/components/pages/ServiceUnavailablePage.jsx +++ /dev/null @@ -1,34 +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 "./MainPage.scss"; - -class ServiceUnavailablePage extends Component { - - state = {}; - - render() { - return ( - <div className="main-page"> - - </div> - ); - } -} - -export default ServiceUnavailablePage; 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; |