diff options
-rw-r--r-- | connection_streams_controller.go | 37 | ||||
-rw-r--r-- | frontend/src/components/ConnectionContent.js | 82 | ||||
-rw-r--r-- | frontend/src/components/ConnectionContent.scss | 20 | ||||
-rw-r--r-- | frontend/src/components/MessageAction.js | 31 | ||||
-rw-r--r-- | frontend/src/components/MessageAction.scss | 11 | ||||
-rw-r--r-- | parsers/http_request_parser.go | 21 | ||||
-rw-r--r-- | parsers/http_response_parser.go | 31 |
7 files changed, 144 insertions, 89 deletions
diff --git a/connection_streams_controller.go b/connection_streams_controller.go index c4876b1..9d73b0e 100644 --- a/connection_streams_controller.go +++ b/connection_streams_controller.go @@ -93,7 +93,7 @@ func (csc ConnectionStreamsController) GetConnectionPayload(c context.Context, c if clientBlocksIndex < len(clientStream.BlocksIndexes)-1 { end = clientStream.BlocksIndexes[clientBlocksIndex+1] } else { - end = len(clientStream.Payload) - 1 + end = len(clientStream.Payload) } size := uint64(end - start) @@ -117,7 +117,7 @@ func (csc ConnectionStreamsController) GetConnectionPayload(c context.Context, c if serverBlocksIndex < len(serverStream.BlocksIndexes)-1 { end = serverStream.BlocksIndexes[serverBlocksIndex+1] } else { - end = len(serverStream.Payload) - 1 + end = len(serverStream.Payload) } size := uint64(end - start) @@ -137,7 +137,18 @@ func (csc ConnectionStreamsController) GetConnectionPayload(c context.Context, c sideChanged, lastClient, lastServer = lastClient, false, true } - if sideChanged { + if !hasClientBlocks() { + clientDocumentIndex++ + clientBlocksIndex = 0 + clientStream = csc.getConnectionStream(c, connectionID, true, clientDocumentIndex) + } + if !hasServerBlocks() { + serverDocumentIndex++ + serverBlocksIndex = 0 + serverStream = csc.getConnectionStream(c, connectionID, false, serverDocumentIndex) + } + + updateMetadata := func() { metadata := parsers.Parse(contentChunkBuffer.Bytes()) var isMetadataContinuation bool for _, elem := range payloadsBuffer { @@ -149,28 +160,26 @@ func (csc ConnectionStreamsController) GetConnectionPayload(c context.Context, c payloadsBuffer = payloadsBuffer[:0] contentChunkBuffer.Reset() } + + if sideChanged { + updateMetadata() + } payloadsBuffer = append(payloadsBuffer, payload) contentChunkBuffer.Write(lastContentSlice) + if clientStream.ID.IsZero() && serverStream.ID.IsZero() { + updateMetadata() + } + if globalIndex > format.Skip { // problem: waste of time if the payload is discarded payloads = append(payloads, payload) } if globalIndex > format.Skip+format.Limit { // problem: the last chunk is not parsed, but can be ok because it is not finished + updateMetadata() return payloads } - - if !hasClientBlocks() { - clientDocumentIndex++ - clientBlocksIndex = 0 - clientStream = csc.getConnectionStream(c, connectionID, true, clientDocumentIndex) - } - if !hasServerBlocks() { - serverDocumentIndex++ - serverBlocksIndex = 0 - serverStream = csc.getConnectionStream(c, connectionID, false, serverDocumentIndex) - } } return payloads diff --git a/frontend/src/components/ConnectionContent.js b/frontend/src/components/ConnectionContent.js index 51dbb67..20ec92b 100644 --- a/frontend/src/components/ConnectionContent.js +++ b/frontend/src/components/ConnectionContent.js @@ -2,7 +2,6 @@ import React, {Component} from 'react'; import './ConnectionContent.scss'; import {Button, Dropdown, Row} from 'react-bootstrap'; import axios from 'axios'; -import {timestampToDateTime, timestampToTime} from "../utils"; import MessageAction from "./MessageAction"; const classNames = require('classnames'); @@ -15,7 +14,7 @@ class ConnectionContent extends Component { loading: false, connectionContent: null, format: "default", - decoded: false, + tryParse: true, messageActionDialog: null }; @@ -43,16 +42,12 @@ class ConnectionContent extends Component { } } - toggleDecoded() { - 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>; + return <span style={{"fontSize": "12px"}}>**already parsed in previous messages**</span>; } let unrollMap = (obj) => obj == null ? null : Object.entries(obj).map(([key, value]) => @@ -62,16 +57,17 @@ class ConnectionContent extends Component { 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>; + let url = <i><u><a href={"http://" + m.host + m.url} target="_blank" + rel="noopener noreferrer">{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> + <p style={{"marginBottom": "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> + <p style={{"marginBottom": "7px"}}>{m.protocol} <strong>{m.status}</strong></p> {unrollMap(m.headers)} <div style={{"margin": "20px 0"}}>{m.body}</div> {unrollMap(m.trailers)} @@ -87,9 +83,10 @@ class ConnectionContent extends Component { } return Object.entries(connectionMessage.metadata["reproducers"]).map(([actionName, actionValue]) => - <Button size="sm" onClick={() => { + <Button size="sm" key={actionName + "_button"} onClick={() => { this.setState({ - messageActionDialog: <MessageAction actionName={actionName} actionValue={actionValue} onHide={() => this.setState({messageActionDialog: null})}/> + messageActionDialog: <MessageAction actionName={actionName} actionValue={actionValue} + onHide={() => this.setState({messageActionDialog: null})}/> }); }}>{actionName}</Button> ); @@ -115,8 +112,9 @@ class ConnectionContent extends Component { </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 + className={classNames("message-content", this.state.decoded ? "message-parsed" : "message-original")}> + {this.state.tryParse && this.state.format === "default" ? this.tryParseConnectionMessage(c) : c.content} </div> </div> ); @@ -129,25 +127,55 @@ class ConnectionContent extends Component { <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"> + <div className="header-actions col-auto"> + <Dropdown onSelect={this.setFormat}> + <Dropdown.Toggle size="sm" id="connection-content-format"> 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.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> + </Dropdown> + + <Dropdown> + <Dropdown.Toggle size="sm" id="connection-content-view"> + view_as + </Dropdown.Toggle> + + <Dropdown.Menu> + <Dropdown.Item eventKey="default" active={true}>default</Dropdown.Item> </Dropdown.Menu> - <Button size="sm" onClick={() => this.toggleDecoded()}>{this.state.decoded ? "Encode" : "Decode"}</Button> + </Dropdown> + + <Dropdown> + <Dropdown.Toggle size="sm" id="connection-content-download"> + download_as + </Dropdown.Toggle> + + <Dropdown.Menu> + <Dropdown.Item eventKey="nl_separated">nl_separated</Dropdown.Item> + <Dropdown.Item eventKey="only_client">only_client</Dropdown.Item> + <Dropdown.Item eventKey="only_server">only_server</Dropdown.Item> + </Dropdown.Menu> </Dropdown> </div> diff --git a/frontend/src/components/ConnectionContent.scss b/frontend/src/components/ConnectionContent.scss index 6354bee..8ee31ec 100644 --- a/frontend/src/components/ConnectionContent.scss +++ b/frontend/src/components/ConnectionContent.scss @@ -11,6 +11,11 @@ overflow-x: hidden; height: calc(100% - 31px); padding: 0 10px; + + p { + margin: 0; + padding: 0; + } } .connection-message { @@ -45,13 +50,6 @@ padding: 10px; } - .message-parsed { - p { - margin: 0; - padding: 0; - } - } - &:hover .connection-message-actions { display: block; } @@ -96,7 +94,13 @@ .header-info { padding-top: 5px; padding-left: 20px; - font-size: 13px; + font-size: 12px; + } + + .header-actions { + .dropdown { + display: inline-block; + } } } diff --git a/frontend/src/components/MessageAction.js b/frontend/src/components/MessageAction.js index 66350c6..2c85d84 100644 --- a/frontend/src/components/MessageAction.js +++ b/frontend/src/components/MessageAction.js @@ -4,7 +4,21 @@ import {Button, FormControl, InputGroup, Modal} from "react-bootstrap"; class MessageAction extends Component { + constructor(props) { + super(props); + this.state = { + copyButtonText: "copy" + }; + this.actionValue = React.createRef(); + this.copyActionValue = this.copyActionValue.bind(this); + } + copyActionValue() { + this.actionValue.current.select(); + document.execCommand('copy'); + this.setState({copyButtonText: "copied!"}); + setTimeout(() => this.setState({copyButtonText: "copy"}), 3000); + } render() { return ( @@ -21,20 +35,13 @@ class MessageAction extends Component { </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>*/} + <InputGroup> + <FormControl as="textarea" className="message-action-value" readOnly={true} + style={{"height": "300px"}} value={this.props.actionValue} ref={this.actionValue} /> + </InputGroup> </Modal.Body> <Modal.Footer className="dialog-footer"> - <Button variant="green" onClick={this.copyActionValue}>copy</Button> + <Button variant="green" onClick={this.copyActionValue}>{this.state.copyButtonText}</Button> <Button variant="red" onClick={this.props.onHide}>close</Button> </Modal.Footer> </Modal> diff --git a/frontend/src/components/MessageAction.scss b/frontend/src/components/MessageAction.scss index df3af8d..f3a8772 100644 --- a/frontend/src/components/MessageAction.scss +++ b/frontend/src/components/MessageAction.scss @@ -1,11 +1,8 @@ @import '../colors.scss'; .message-action-value { - pre { - font-size: 13px; - padding: 15px; - background-color: $color-primary-2; - color: $color-primary-4; - } - + font-size: 13px; + padding: 15px; + background-color: $color-primary-2; + color: $color-primary-4; }
\ No newline at end of file diff --git a/parsers/http_request_parser.go b/parsers/http_request_parser.go index cfac196..e2224b8 100644 --- a/parsers/http_request_parser.go +++ b/parsers/http_request_parser.go @@ -4,6 +4,7 @@ import ( "bufio" "bytes" "encoding/json" + log "github.com/sirupsen/logrus" "io/ioutil" "moul.io/http2curl" "net/http" @@ -41,12 +42,13 @@ func (p HttpRequestParser) TryParse(content []byte) Metadata { return nil } var body string - if request.Body != nil { - if buffer, err := ioutil.ReadAll(request.Body); err == nil { - body = string(buffer) - } - _ = request.Body.Close() + if buffer, err := ioutil.ReadAll(request.Body); err == nil { + body = string(buffer) + } else { + log.WithError(err).Error("failed to read body in http_request_parser") + return nil } + _ = request.Body.Close() _ = request.ParseForm() return HttpRequestMetadata{ @@ -62,18 +64,21 @@ func (p HttpRequestParser) TryParse(content []byte) Metadata { Body: body, Trailer: JoinArrayMap(request.Trailer), Reproducers: HttpRequestMetadataReproducers{ - CurlCommand: curlCommand(request), + CurlCommand: curlCommand(content), RequestsCode: requestsCode(request), FetchRequest: fetchRequest(request, body), }, } } -func curlCommand(request *http.Request) string { +func curlCommand(content []byte) string { + // a new reader is required because all the body is read before and GetBody() doesn't works + reader := bufio.NewReader(bytes.NewReader(content)) + request, _ := http.ReadRequest(reader) if command, err := http2curl.GetCurlCommand(request); err == nil { return command.String() } else { - return "invalid-request" + return err.Error() } } diff --git a/parsers/http_response_parser.go b/parsers/http_response_parser.go index a639dec..1770116 100644 --- a/parsers/http_response_parser.go +++ b/parsers/http_response_parser.go @@ -4,6 +4,7 @@ import ( "bufio" "bytes" "compress/gzip" + log "github.com/sirupsen/logrus" "io/ioutil" "net/http" ) @@ -33,23 +34,27 @@ func (p HttpResponseParser) TryParse(content []byte) Metadata { } var body string var compressed bool - if response.Body != nil { - switch response.Header.Get("Content-Encoding") { - case "gzip": - if gzipReader, err := gzip.NewReader(response.Body); err == nil { - if buffer, err := ioutil.ReadAll(gzipReader); err == nil { - body = string(buffer) - compressed = true - } - _ = gzipReader.Close() - } - default: - if buffer, err := ioutil.ReadAll(response.Body); err == nil { + switch response.Header.Get("Content-Encoding") { + case "gzip": + if gzipReader, err := gzip.NewReader(response.Body); err == nil { + if buffer, err := ioutil.ReadAll(gzipReader); err == nil { body = string(buffer) + compressed = true + } else { + log.WithError(err).Error("failed to read gzipped body in http_response_parser") + return nil } + _ = gzipReader.Close() + } + default: + if buffer, err := ioutil.ReadAll(response.Body); err == nil { + body = string(buffer) + } else { + log.WithError(err).Error("failed to read body in http_response_parser") + return nil } - _ = response.Body.Close() } + _ = response.Body.Close() var location string if locationUrl, err := response.Location(); err == nil { |