/*
* This file is part of caronte (https://github.com/eciavatta/caronte).
* Copyright (c) 2020 Emiliano Ciavatta.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
import DOMPurify from "dompurify";
import React, { Component } from "react";
import { Row } from "react-bootstrap";
import ReactJson from "react-json-view";
import backend from "../../backend";
import log from "../../log";
import rules from "../../model/rules";
import { downloadBlob, getHeaderValue } from "../../utils";
import ButtonField from "../fields/ButtonField";
import ChoiceField from "../fields/ChoiceField";
import CopyDialog from "../dialogs/CopyDialog";
import "./StreamsPane.scss";
import reactStringReplace from "react-string-replace";
import classNames from "classnames";
class StreamsPane extends Component {
state = {
messages: [],
format: "default",
tryParse: true,
};
constructor(props) {
super(props);
this.validFormats = [
"default",
"hex",
"hexdump",
"base32",
"base64",
"ascii",
"binary",
"decimal",
"octal",
];
}
componentDidMount() {
if (
this.props.connection &&
this.state.currentId !== this.props.connection.id
) {
this.setState({ currentId: this.props.connection.id });
this.loadStream(this.props.connection.id);
}
document.title = "caronte:~/$";
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (
this.props.connection &&
(this.props.connection !== prevProps.connection ||
this.state.format !== prevState.format)
) {
this.closeRenderWindow();
this.loadStream(this.props.connection.id);
}
}
componentWillUnmount() {
this.closeRenderWindow();
}
loadStream = (connectionId) => {
this.setState({ messages: [], currentId: connectionId });
backend
.get(`/api/streams/${connectionId}?format=${this.state.format}`)
.then((res) => this.setState({ messages: res.json }));
};
setFormat = (format) => {
if (this.validFormats.includes(format)) {
this.setState({ format });
}
};
viewAs = (mode) => {
if (mode === "decoded") {
this.setState({ tryParse: true });
} else if (mode === "raw") {
this.setState({ tryParse: false });
}
};
tryParseConnectionMessage = (connectionMessage) => {
const isClient = connectionMessage["from_client"];
if (connectionMessage.metadata == null) {
return this.highlightRules(connectionMessage.content, isClient);
}
let unrollMap = (obj) =>
obj == null
? null
: Object.entries(obj).map(([key, value]) => (
{key}: {value}
));
let m = connectionMessage.metadata;
switch (m.type) {
case "http-request":
let url = (
{m.host}
{m.url}
);
return (
{m.method} {url} {m.protocol}
{unrollMap(m.headers)}
{this.highlightRules(m.body, isClient)}
{unrollMap(m.trailers)}
);
case "http-response":
const contentType = getHeaderValue(m, "Content-Type");
let body = m.body;
if (contentType && contentType.includes("application/json")) {
try {
const json = JSON.parse(m.body);
if (typeof json === "object") {
body = (
);
}
} catch (e) {
log.error(e);
}
}
return (
{m.protocol} {m.status}
{unrollMap(m.headers)}
{this.highlightRules(body, isClient)}
{unrollMap(m.trailers)}
);
default:
return this.highlightRules(connectionMessage.content, isClient);
}
};
highlightRules = (content, isClient) => {
let streamContent = content;
this.props.connection["matched_rules"].forEach((ruleId) => {
const rule = rules.ruleById(ruleId);
rule.patterns.forEach((pattern) => {
if (
(!isClient && pattern.direction === 1) ||
(isClient && pattern.direction === 2)
) {
return;
}
let flags = "";
pattern["caseless"] && (flags += "i");
pattern["dot_all"] && (flags += "s");
pattern["multi_line"] && (flags += "m");
pattern["unicode_property"] && (flags += "u");
const regex = new RegExp(
pattern.regex.replace(/^\//, "(").replace(/\/$/, ")"),
flags
);
streamContent = reactStringReplace(streamContent, regex, (match, i) => (
{match}
));
});
});
return streamContent;
};
connectionsActions = (connectionMessage) => {
if (!connectionMessage.metadata) {
return null;
}
const m = connectionMessage.metadata;
switch (m.type) {
case "http-request":
if (!connectionMessage.metadata["reproducers"]) {
return;
}
return Object.entries(connectionMessage.metadata["reproducers"]).map(
([name, value]) => (
{
this.setState({
messageActionDialog: (
this.setState({ messageActionDialog: null })
}
/>
),
});
}}
/>
)
);
case "http-response":
const contentType = getHeaderValue(m, "Content-Type");
if (contentType && contentType.includes("text/html")) {
return (
{
let w;
if (
this.state.renderWindow &&
!this.state.renderWindow.closed
) {
w = this.state.renderWindow;
} else {
w = window.open(
"",
"",
"width=900, height=600, scrollbars=yes"
);
this.setState({ renderWindow: w });
}
w.document.body.innerHTML = DOMPurify.sanitize(m.body);
w.focus();
}}
/>
);
}
break;
default:
return null;
}
};
downloadStreamRaw = (value) => {
if (this.state.currentId) {
backend
.download(
`/api/streams/${this.props.connection.id}/download?format=${this.state.format}&type=${value}`
)
.then((res) =>
downloadBlob(
res.blob,
`${this.state.currentId}-${value}-${this.state.format}.txt`
)
)
.catch((_) => log.error("Failed to download stream messages"));
}
};
closeRenderWindow = () => {
if (this.state.renderWindow) {
this.state.renderWindow.close();
}
};
render() {
const conn = this.props.connection || {
ip_src: "0.0.0.0",
ip_dst: "0.0.0.0",
port_src: "0",
port_dst: "0",
started_at: new Date().toISOString(),
};
const content = this.state.messages || [];
let payload = content
.filter(
(c) =>
!this.state.tryParse ||
(this.state.tryParse && !c["is_metadata_continuation"])
)
.map((c, i) => (
offset: {c.index}
{" "}
|{" "}
timestamp: {c.timestamp}
{" "}
|{" "}
retransmitted:{" "}
{c["is_retransmitted"] ? "yes" : "no"}
{this.connectionsActions(c)}
{c["from_client"] ? "client" : "server"}
{this.state.tryParse && this.state.format === "default"
? this.tryParseConnectionMessage(c)
: c.content}
));
return (
flow: {conn["ip_src"]}:{conn["port_src"]} ->{" "}
{conn["ip_dst"]}:{conn["port_dst"]}
{" "}
| timestamp: {conn["started_at"]}
{payload}
{this.state.messageActionDialog}
);
}
}
export default StreamsPane;