aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEmiliano Ciavatta2020-09-16 13:41:04 +0000
committerEmiliano Ciavatta2020-09-16 13:41:04 +0000
commitec949ffea86a14526a7142d048022a4a07f684ff (patch)
treef91966e42d93e94e29d7c11c1eeb67704da297eb
parent2954045cb28ea8cbf4dbd019355a2df8fed28ccc (diff)
Improve frontend connection visualization
-rw-r--r--connection_streams_controller.go18
-rw-r--r--frontend/src/components/Connection.js7
-rw-r--r--frontend/src/components/ConnectionContent.js146
-rw-r--r--frontend/src/components/ConnectionContent.scss101
-rw-r--r--frontend/src/components/MessageAction.js45
-rw-r--r--frontend/src/components/MessageAction.scss11
-rw-r--r--frontend/src/utils.js5
-rw-r--r--parsers/http_request_parser.go2
8 files changed, 269 insertions, 66 deletions
diff --git a/connection_streams_controller.go b/connection_streams_controller.go
index 3ba30f8..c4876b1 100644
--- a/connection_streams_controller.go
+++ b/connection_streams_controller.go
@@ -27,13 +27,14 @@ type ConnectionStream struct {
type PatternSlice [2]uint64
type Payload struct {
- FromClient bool `json:"from_client"`
- Content string `json:"content"`
- Metadata parsers.Metadata `json:"metadata"`
- Index int `json:"index"`
- Timestamp time.Time `json:"timestamp"`
- IsRetransmitted bool `json:"is_retransmitted"`
- RegexMatches []RegexSlice `json:"regex_matches"`
+ FromClient bool `json:"from_client"`
+ Content string `json:"content"`
+ Metadata parsers.Metadata `json:"metadata"`
+ IsMetadataContinuation bool `json:"is_metadata_continuation"`
+ Index int `json:"index"`
+ Timestamp time.Time `json:"timestamp"`
+ IsRetransmitted bool `json:"is_retransmitted"`
+ RegexMatches []RegexSlice `json:"regex_matches"`
}
type RegexSlice struct {
@@ -138,8 +139,11 @@ func (csc ConnectionStreamsController) GetConnectionPayload(c context.Context, c
if sideChanged {
metadata := parsers.Parse(contentChunkBuffer.Bytes())
+ var isMetadataContinuation bool
for _, elem := range payloadsBuffer {
elem.Metadata = metadata
+ elem.IsMetadataContinuation = isMetadataContinuation
+ isMetadataContinuation = true
}
payloadsBuffer = payloadsBuffer[:0]
diff --git a/frontend/src/components/Connection.js b/frontend/src/components/Connection.js
index e41f542..93c6438 100644
--- a/frontend/src/components/Connection.js
+++ b/frontend/src/components/Connection.js
@@ -57,6 +57,11 @@ class Connection extends Component {
let closedAt = new Date(conn.closed_at);
let processedAt = new Date(conn.processed_at);
let duration = ((closedAt - startedAt) / 1000).toFixed(3);
+ if (duration > 1000 || duration < -1000) {
+ duration = "∞";
+ } else {
+ duration += "s";
+ }
let timeInfo = <div>
<span>Started at {startedAt.toLocaleDateString() + " " + startedAt.toLocaleTimeString()}</span><br/>
<span>Processed at {processedAt.toLocaleDateString() + " " + processedAt.toLocaleTimeString()}</span><br/>
@@ -106,7 +111,7 @@ class Connection extends Component {
<td className="clickable" onClick={this.props.onSelected}>
<OverlayTrigger trigger={["focus", "hover"]} placement="right"
overlay={popoverFor("duration", timeInfo)}>
- <span className="test-tooltip">{duration}s</span>
+ <span className="test-tooltip">{duration}</span>
</OverlayTrigger>
</td>
<td className="clickable" onClick={this.props.onSelected}>{conn.client_bytes}</td>
diff --git a/frontend/src/components/ConnectionContent.js b/frontend/src/components/ConnectionContent.js
index 2100a68..51dbb67 100644
--- a/frontend/src/components/ConnectionContent.js
+++ b/frontend/src/components/ConnectionContent.js
@@ -1,7 +1,11 @@
import React, {Component} from 'react';
import './ConnectionContent.scss';
-import {Dropdown, Button} from 'react-bootstrap';
+import {Button, Dropdown, Row} from 'react-bootstrap';
import axios from 'axios';
+import {timestampToDateTime, timestampToTime} from "../utils";
+import MessageAction from "./MessageAction";
+
+const classNames = require('classnames');
class ConnectionContent extends Component {
@@ -12,6 +16,7 @@ class ConnectionContent extends Component {
connectionContent: null,
format: "default",
decoded: false,
+ messageActionDialog: null
};
this.validFormats = ["default", "hex", "hexdump", "base32", "base64", "ascii", "binary", "decimal", "octal"];
@@ -42,62 +47,115 @@ class ConnectionContent extends Component {
this.setState({decoded: !this.state.decoded});
}
+ tryParseConnectionMessage(connectionMessage) {
+ if (connectionMessage.metadata == null) {
+ return connectionMessage.content;
+ }
+ if (connectionMessage["is_metadata_continuation"]) {
+ return <span>already parsed in previous messages</span>;
+ }
+
+ let unrollMap = (obj) => obj == null ? null : Object.entries(obj).map(([key, value]) =>
+ <p><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">{m.host}{m.url}</a></u></i>;
+ return <span className="type-http-request">
+ <p style={{"margin-bottom": "7px"}}><strong>{m.method}</strong> {url} {m.protocol}</p>
+ {unrollMap(m.headers)}
+ <div style={{"margin": "20px 0"}}>{m.body}</div>
+ {unrollMap(m.trailers)}
+ </span>;
+ case "http-response":
+ return <span className="type-http-response">
+ <p style={{"margin-bottom": "7px"}}>{m.protocol} <strong>{m.status}</strong></p>
+ {unrollMap(m.headers)}
+ <div style={{"margin": "20px 0"}}>{m.body}</div>
+ {unrollMap(m.trailers)}
+ </span>;
+ default:
+ return connectionMessage.content;
+ }
+ }
+
+ connectionsActions(connectionMessage) {
+ if (connectionMessage.metadata == null || connectionMessage.metadata["reproducers"] === undefined) {
+ return null;
+ }
+
+ return Object.entries(connectionMessage.metadata["reproducers"]).map(([actionName, actionValue]) =>
+ <Button size="sm" onClick={() => {
+ this.setState({
+ messageActionDialog: <MessageAction actionName={actionName} actionValue={actionValue} onHide={() => this.setState({messageActionDialog: null})}/>
+ });
+ }}>{actionName}</Button>
+ );
+ }
+
render() {
let content = this.state.connectionContent;
- if (content === null) {
- return <div>nope</div>;
+ if (content == null) {
+ return <div>select a connection to view</div>;
}
let payload = content.map((c, i) =>
- <span key={`content-${i}`} className={c.from_client ? "from-client" : "from-server"}>
- {c.from_client
- ?
- <div id="content">{c.content}</div>
- :
- <>
- {c.decoded_content
- ?
- <>
- <div style={{display: this.state.decoded ? 'none':'inherit'}} id="content">{c.content}</div>
- <div style={{display: this.state.decoded ? 'inherit':'none'}} id="decoded_content">{c.decoded_content}</div>
- </>
- :
- <div id="content">{c.content}</div>
- }
- </>
- }
- </span>
+ <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={classNames("message-content", this.state.decoded ? "message-parsed" : "message-original")}>
+ {this.state.decoded ? this.tryParseConnectionMessage(c) : c.content}
+ </div>
+ </div>
);
return (
<div className="connection-content">
- <div className="connection-content-options">
- <Dropdown onSelect={this.setFormat} >
- <Dropdown.Toggle size="sm" id="dropdown-basic">
- format
- </Dropdown.Toggle>
-
- <Dropdown.Menu>
- <Dropdown.Item eventKey="default" active={this.state.format === "default"}>plain</Dropdown.Item>
- <Dropdown.Item eventKey="hex" active={this.state.format === "hex"}>hex</Dropdown.Item>
- <Dropdown.Item eventKey="hexdump" active={this.state.format === "hexdump"}>hexdump</Dropdown.Item>
- <Dropdown.Item eventKey="base32" active={this.state.format === "base32"}>base32</Dropdown.Item>
- <Dropdown.Item eventKey="base64" active={this.state.format === "base64"}>base64</Dropdown.Item>
- <Dropdown.Item eventKey="ascii" active={this.state.format === "ascii"}>ascii</Dropdown.Item>
- <Dropdown.Item eventKey="binary" active={this.state.format === "binary"}>binary</Dropdown.Item>
- <Dropdown.Item eventKey="decimal" active={this.state.format === "decimal"}>decimal</Dropdown.Item>
- <Dropdown.Item eventKey="octal" active={this.state.format === "octal"}>octal</Dropdown.Item>
- </Dropdown.Menu>
- <Button onClick={() => this.toggleDecoded()}>{this.state.decoded ? "Encode" : "Decode"}</Button>
-
-
- </Dropdown>
-
-
+ <div className="connection-content-header container-fluid">
+ <Row>
+ <div className="header-info col">
+ <span><strong>flow</strong>: {this.props.connection.ip_src}:{this.props.connection.port_src} -> {this.props.connection.ip_dst}:{this.props.connection.port_dst}</span>
+ <span> | <strong>timestamp</strong>: {this.props.connection.started_at}</span>
+ </div>
+ <div className="col-auto">
+ <Dropdown onSelect={this.setFormat} >
+ <Dropdown.Toggle size="sm" id="dropdown-basic">
+ format
+ </Dropdown.Toggle>
+
+ <Dropdown.Menu>
+ <Dropdown.Item eventKey="default" active={this.state.format === "default"}>plain</Dropdown.Item>
+ <Dropdown.Item eventKey="hex" active={this.state.format === "hex"}>hex</Dropdown.Item>
+ <Dropdown.Item eventKey="hexdump" active={this.state.format === "hexdump"}>hexdump</Dropdown.Item>
+ <Dropdown.Item eventKey="base32" active={this.state.format === "base32"}>base32</Dropdown.Item>
+ <Dropdown.Item eventKey="base64" active={this.state.format === "base64"}>base64</Dropdown.Item>
+ <Dropdown.Item eventKey="ascii" active={this.state.format === "ascii"}>ascii</Dropdown.Item>
+ <Dropdown.Item eventKey="binary" active={this.state.format === "binary"}>binary</Dropdown.Item>
+ <Dropdown.Item eventKey="decimal" active={this.state.format === "decimal"}>decimal</Dropdown.Item>
+ <Dropdown.Item eventKey="octal" active={this.state.format === "octal"}>octal</Dropdown.Item>
+ </Dropdown.Menu>
+ <Button size="sm" onClick={() => this.toggleDecoded()}>{this.state.decoded ? "Encode" : "Decode"}</Button>
+
+
+ </Dropdown>
+ </div>
+ </Row>
</div>
<pre>{payload}</pre>
+ {this.state.messageActionDialog}
</div>
);
}
diff --git a/frontend/src/components/ConnectionContent.scss b/frontend/src/components/ConnectionContent.scss
index 5a17066..6354bee 100644
--- a/frontend/src/components/ConnectionContent.scss
+++ b/frontend/src/components/ConnectionContent.scss
@@ -1,29 +1,104 @@
@import '../colors.scss';
.connection-content {
- background-color: $color-primary-3;
+ background-color: $color-primary-0;
+ padding: 10px 10px 0;
height: 100%;
- overflow: fixed;
pre {
- background-color: $color-primary-0;
- padding: 10px 20px;
word-break: break-word;
- max-width: 100%;
white-space: pre-wrap;
- height: 95%;
+ overflow-x: hidden;
+ height: calc(100% - 31px);
+ padding: 0 10px;
}
- .from-client {
- color: #d4e0fc;
- }
+ .connection-message {
+ border: 4px solid $color-primary-3;
+ border-top: 0;
+ margin: 10px 0;
+ position: relative;
+
+ .connection-message-header {
+ background-color: $color-primary-3;
+ height: 25px;
+
+ .connection-message-info {
+ font-size: 11px;
+ margin-left: -10px;
+ margin-top: 6px;
+ }
- .from-server {
- color: $color-secondary-4;
+ .connection-message-actions {
+ margin-right: -18px;
+ display: none;
- &:hover {
+ button {
+ margin: 0 3px;
+ font-size: 11px;
+ padding: 5px;
+ }
+ }
+ }
+
+ .message-content {
+ padding: 10px;
+ }
+
+ .message-parsed {
+ p {
+ margin: 0;
+ padding: 0;
+ }
+ }
+
+ &:hover .connection-message-actions {
+ display: block;
+ }
+
+ .connection-message-label {
+ position: absolute;
background-color: $color-primary-3;
- border-top: 1px solid $color-primary-1;
+ top: 0;
+ padding: 10px 0;
+ font-size: 12px;
+
+ writing-mode: vertical-rl;
+ text-orientation: mixed;
}
+
+ &.from-client {
+ color: $color-primary-4;
+ margin-right: 100px;
+
+ .connection-message-label {
+ right: -22px;
+ }
+ }
+
+ &.from-server {
+ color: $color-primary-4;
+ margin-left: 100px;
+
+ .connection-message-label {
+ left: -22px;
+ transform: rotate(-180deg);
+ }
+ }
+
}
+
+ .connection-content-header {
+ background-color: $color-primary-2;
+ padding: 0;
+ height: 31px;
+
+ .header-info {
+ padding-top: 5px;
+ padding-left: 20px;
+ font-size: 13px;
+ }
+ }
+
+
}
diff --git a/frontend/src/components/MessageAction.js b/frontend/src/components/MessageAction.js
new file mode 100644
index 0000000..66350c6
--- /dev/null
+++ b/frontend/src/components/MessageAction.js
@@ -0,0 +1,45 @@
+import React, {Component} from 'react';
+import './MessageAction.scss';
+import {Button, FormControl, InputGroup, Modal} from "react-bootstrap";
+
+class MessageAction extends Component {
+
+
+
+ render() {
+ return (
+ <Modal
+ {...this.props}
+ show="true"
+ size="lg"
+ aria-labelledby="message-action-dialog"
+ centered
+ >
+ <Modal.Header>
+ <Modal.Title id="message-action-dialog">
+ {this.props.actionName}
+ </Modal.Title>
+ </Modal.Header>
+ <Modal.Body>
+ <div className="message-action-value">
+ <pre>
+ {this.props.actionValue}
+ </pre>
+ </div>
+
+ {/*<InputGroup>*/}
+ {/* <FormControl as="textarea" className="message-action-value" readOnly={true}*/}
+ {/* style={{"height": "300px"}}*/}
+ {/* value={this.props.actionValue}/>*/}
+ {/*</InputGroup>*/}
+ </Modal.Body>
+ <Modal.Footer className="dialog-footer">
+ <Button variant="green" onClick={this.copyActionValue}>copy</Button>
+ <Button variant="red" onClick={this.props.onHide}>close</Button>
+ </Modal.Footer>
+ </Modal>
+ );
+ }
+}
+
+export default MessageAction;
diff --git a/frontend/src/components/MessageAction.scss b/frontend/src/components/MessageAction.scss
new file mode 100644
index 0000000..df3af8d
--- /dev/null
+++ b/frontend/src/components/MessageAction.scss
@@ -0,0 +1,11 @@
+@import '../colors.scss';
+
+.message-action-value {
+ pre {
+ font-size: 13px;
+ padding: 15px;
+ background-color: $color-primary-2;
+ color: $color-primary-4;
+ }
+
+} \ No newline at end of file
diff --git a/frontend/src/utils.js b/frontend/src/utils.js
index 26c10d3..7381f69 100644
--- a/frontend/src/utils.js
+++ b/frontend/src/utils.js
@@ -59,3 +59,8 @@ export function timestampToTime(timestamp) {
let seconds = "0" + d.getSeconds();
return hours + ':' + minutes.substr(-2) + ':' + seconds.substr(-2);
}
+
+export function timestampToDateTime(timestamp) {
+ let d = new Date(timestamp);
+ return d.toLocaleDateString() + " " + d.toLocaleTimeString();
+}
diff --git a/parsers/http_request_parser.go b/parsers/http_request_parser.go
index d204d4c..cfac196 100644
--- a/parsers/http_request_parser.go
+++ b/parsers/http_request_parser.go
@@ -136,7 +136,7 @@ func fetchRequest(request *http.Request, body string) string {
}
func toJson(obj interface{}) string {
- if buffer, err := json.Marshal(obj); err == nil {
+ if buffer, err := json.MarshalIndent(obj, "", "\t"); err == nil {
return string(buffer)
} else {
return ""