/* * 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;