aboutsummaryrefslogtreecommitdiff
path: root/frontend/src/components/panels/StreamsPane.jsx
diff options
context:
space:
mode:
authorVaiTon2023-06-05 14:54:12 +0000
committerVaiTon2023-06-05 14:54:12 +0000
commit2e6af3b2623da3d002816a6de325133d626858c9 (patch)
tree69448ca3dc33f8754fa88ffa77ed3dea524848b5 /frontend/src/components/panels/StreamsPane.jsx
parentf4c97974dac97f3254047fd17abed09e82cae797 (diff)
Frontend to jsx
Diffstat (limited to 'frontend/src/components/panels/StreamsPane.jsx')
-rw-r--r--frontend/src/components/panels/StreamsPane.jsx453
1 files changed, 453 insertions, 0 deletions
diff --git a/frontend/src/components/panels/StreamsPane.jsx b/frontend/src/components/panels/StreamsPane.jsx
new file mode 100644
index 0000000..9e88f55
--- /dev/null
+++ b/frontend/src/components/panels/StreamsPane.jsx
@@ -0,0 +1,453 @@
+/*
+ * This file is part of caronte (https://github.com/eciavatta/caronte).
+ * Copyright (c) 2020 Emiliano Ciavatta.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <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;