From ec949ffea86a14526a7142d048022a4a07f684ff Mon Sep 17 00:00:00 2001 From: Emiliano Ciavatta Date: Wed, 16 Sep 2020 15:41:04 +0200 Subject: Improve frontend connection visualization --- connection_streams_controller.go | 18 +-- frontend/src/components/Connection.js | 7 +- frontend/src/components/ConnectionContent.js | 146 +++++++++++++++++-------- frontend/src/components/ConnectionContent.scss | 101 ++++++++++++++--- frontend/src/components/MessageAction.js | 45 ++++++++ frontend/src/components/MessageAction.scss | 11 ++ frontend/src/utils.js | 5 + parsers/http_request_parser.go | 2 +- 8 files changed, 269 insertions(+), 66 deletions(-) create mode 100644 frontend/src/components/MessageAction.js create mode 100644 frontend/src/components/MessageAction.scss 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 =
Started at {startedAt.toLocaleDateString() + " " + startedAt.toLocaleTimeString()}
Processed at {processedAt.toLocaleDateString() + " " + processedAt.toLocaleTimeString()}
@@ -106,7 +111,7 @@ class Connection extends Component { - {duration}s + {duration} {conn.client_bytes} 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 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": + return +

{m.protocol} {m.status}

+ {unrollMap(m.headers)} +
{m.body}
+ {unrollMap(m.trailers)} +
; + 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]) => + + ); + } + render() { let content = this.state.connectionContent; - if (content === null) { - return
nope
; + if (content == null) { + return
select a connection to view
; } let payload = content.map((c, i) => - - {c.from_client - ? -
{c.content}
- : - <> - {c.decoded_content - ? - <> -
{c.content}
-
{c.decoded_content}
- - : -
{c.content}
- } - - } -
+
+
+
+
+ offset: {c.index} | timestamp: {c.timestamp} + | retransmitted: {c["is_retransmitted"] ? "yes" : "no"} +
+
{this.connectionsActions(c)}
+
+
+
{c.from_client ? "client" : "server"}
+
+ {this.state.decoded ? this.tryParseConnectionMessage(c) : c.content} +
+
); return (
-
- - - format - - - - plain - hex - hexdump - base32 - base64 - ascii - binary - decimal - octal - - - - - - - +
+ +
+ flow: {this.props.connection.ip_src}:{this.props.connection.port_src} -> {this.props.connection.ip_dst}:{this.props.connection.port_dst} + | timestamp: {this.props.connection.started_at} +
+
+ + + format + + + + plain + hex + hexdump + base32 + base64 + ascii + binary + decimal + octal + + + + + +
+
{payload}
+ {this.state.messageActionDialog}
); } 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 ( + + + + {this.props.actionName} + + + +
+
+                            {this.props.actionValue}
+                        
+
+ + {/**/} + {/* */} + {/**/} +
+ + + + +
+ ); + } +} + +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 "" -- cgit v1.2.3-70-g09d2