/*
* 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 MessageAction from "../objects/MessageAction";
import "./StreamsPane.scss";
const reactStringReplace = require('react-string-replace')
const classNames = require("classnames");
class StreamsPane extends Component {
state = {
messages: [],
format: "default",
tryParse: true
};
constructor(props) {
super(props);
this.validFormats = ["default", "hex", "hexdump", "base32", "base64", "ascii", "binary", "decimal", "octal"];
}
componentDidMount() {
if (this.props.connection && this.state.currentId !== this.props.connection.id) {
this.setState({currentId: this.props.connection.id});
this.loadStream(this.props.connection.id);
}
document.title = "caronte:~/$";
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (this.props.connection && (
this.props.connection !== prevProps.connection || this.state.format !== prevState.format)) {
this.closeRenderWindow();
this.loadStream(this.props.connection.id);
}
}
componentWillUnmount() {
this.closeRenderWindow();
}
loadStream = (connectionId) => {
this.setState({messages: [], 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});
}
};
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);
body = ;
} catch (e) {
log.error(e);
body = m.body;
}
}
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 + ")", 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(([actionName, actionValue]) =>
{
this.setState({
messageActionDialog: this.setState({messageActionDialog: null})}/>
});
}}/>
);
case "http-response":
const contentType = getHeaderValue(m, "Content-Type");
if (contentType && contentType.includes("text/html")) {
return {
let w;
if (this.state.renderWindow && !this.state.renderWindow.closed) {
w = this.state.renderWindow;
} else {
w = window.open("", "", "width=900, height=600, scrollbars=yes");
this.setState({renderWindow: w});
}
w.document.body.innerHTML = DOMPurify.sanitize(m.body);
w.focus();
}}/>;
}
break;
default:
return null;
}
};
downloadStreamRaw = (value) => {
if (this.state.currentId) {
backend.download(`/api/streams/${this.props.connection.id}/download?format=${this.state.format}&type=${value}`)
.then((res) => downloadBlob(res.blob, `${this.state.currentId}-${value}-${this.state.format}.txt`))
.catch((_) => log.error("Failed to download stream messages"));
}
};
closeRenderWindow = () => {
if (this.state.renderWindow) {
this.state.renderWindow.close();
}
};
render() {
const conn = this.props.connection || {
"ip_src": "0.0.0.0",
"ip_dst": "0.0.0.0",
"port_src": "0",
"port_dst": "0",
"started_at": new Date().toISOString(),
};
const content = this.state.messages || [];
let payload = content.filter((c) => !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;