/*
* 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 React, {Component} from 'react';
import './ConnectionContent.scss';
import {Row} from 'react-bootstrap';
import MessageAction from "./MessageAction";
import backend from "../backend";
import ButtonField from "./fields/ButtonField";
import ChoiceField from "./fields/ChoiceField";
import DOMPurify from 'dompurify';
import ReactJson from 'react-json-view'
import {downloadBlob, getHeaderValue} from "../utils";
import log from "../log";
const classNames = require('classnames');
class ConnectionContent extends Component {
constructor(props) {
super(props);
this.state = {
loading: false,
connectionContent: null,
format: "default",
tryParse: true,
messageActionDialog: null
};
this.validFormats = ["default", "hex", "hexdump", "base32", "base64", "ascii", "binary", "decimal", "octal"];
}
componentDidMount() {
if (this.props.connection != null) {
this.loadStream();
}
document.title = "caronte:~/$";
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (this.props.connection != null && (
this.props.connection !== prevProps.connection || this.state.format !== prevState.format)) {
this.closeRenderWindow();
this.loadStream();
}
}
componentWillUnmount() {
this.closeRenderWindow();
}
loadStream = () => {
this.setState({loading: true});
// TODO: limit workaround.
backend.get(`/api/streams/${this.props.connection.id}?format=${this.state.format}&limit=999999`).then(res => {
this.setState({
connectionContent: res.json,
loading: false
});
});
};
setFormat = (format) => {
if (this.validFormats.includes(format)) {
this.setState({format: format});
}
};
tryParseConnectionMessage = (connectionMessage) => {
if (connectionMessage.metadata == null) {
return connectionMessage.content;
}
if (connectionMessage["is_metadata_continuation"]) {
return **already parsed in previous messages**;
}
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)}
{m.body}
{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) {
console.log(e);
}
}
return
{m.protocol} {m.status}
{unrollMap(m.headers)}
{body}
{unrollMap(m.trailers)}
;
default:
return connectionMessage.content;
}
};
connectionsActions = (connectionMessage) => {
if (connectionMessage.metadata == null) { //} || !connectionMessage.metadata["reproducers"]) {
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) => {
backend.download(`/api/streams/${this.props.connection.id}/download?format=${this.state.format}&type=${value}`)
.then(res => downloadBlob(res.blob, `${this.props.connection.id}-${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;
const content = this.state.connectionContent;
if (content == null) {
return select a connection to view
;
}
let payload = content.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 ConnectionContent;