From d203f3c7e3bcaa20895c0f32f348cd1513ae9876 Mon Sep 17 00:00:00 2001 From: Emiliano Ciavatta Date: Thu, 8 Oct 2020 22:17:04 +0200 Subject: Frontend folder structure refactor --- frontend/src/components/panels/StreamsPane.js | 242 ++++++++++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 frontend/src/components/panels/StreamsPane.js (limited to 'frontend/src/components/panels/StreamsPane.js') diff --git a/frontend/src/components/panels/StreamsPane.js b/frontend/src/components/panels/StreamsPane.js new file mode 100644 index 0000000..c8bd121 --- /dev/null +++ b/frontend/src/components/panels/StreamsPane.js @@ -0,0 +1,242 @@ +/* + * 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 './StreamsPane.scss'; +import {Row} from 'react-bootstrap'; +import MessageAction from "../objects/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 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: []}); + 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: 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) { + 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) => { + 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.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; -- cgit v1.2.3-70-g09d2 From c21541a31fe45ba3a0bafca46415247f3837713e Mon Sep 17 00:00:00 2001 From: Emiliano Ciavatta Date: Fri, 9 Oct 2020 17:07:24 +0200 Subject: Add MainPane --- Dockerfile | 5 +- VERSION | 1 - application_router.go | 16 ++-- caronte.go | 11 ++- frontend/public/favicon.ico | Bin 34239 -> 12163 bytes frontend/public/logo192.png | Bin 34239 -> 6498 bytes frontend/public/logo512.png | Bin 34239 -> 26806 bytes frontend/src/components/App.js | 5 +- frontend/src/components/Notifications.js | 99 ++++++++++++++------- frontend/src/components/Notifications.scss | 16 +++- frontend/src/components/Timeline.js | 8 +- frontend/src/components/Timeline.scss | 4 + frontend/src/components/dialogs/Filters.js | 7 +- frontend/src/components/fields/ButtonField.js | 4 +- frontend/src/components/fields/TextField.scss | 4 + .../src/components/filters/FiltersDefinitions.js | 38 ++------ frontend/src/components/objects/Connection.js | 39 +++----- frontend/src/components/objects/Connection.scss | 4 + frontend/src/components/objects/LinkPopover.scss | 5 ++ frontend/src/components/pages/MainPage.js | 2 +- frontend/src/components/pages/MainPage.scss | 1 + frontend/src/components/panels/ConnectionsPane.js | 12 ++- .../src/components/panels/ConnectionsPane.scss | 5 +- frontend/src/components/panels/MainPane.js | 82 ++++++++++++++++- frontend/src/components/panels/MainPane.scss | 27 +++++- frontend/src/components/panels/StreamsPane.js | 5 +- frontend/src/components/panels/StreamsPane.scss | 9 +- frontend/src/components/panels/common.scss | 8 ++ frontend/src/index.scss | 12 +++ frontend/src/logo.svg | 8 +- notification_controller.go | 8 +- resources_controller.go | 2 +- 32 files changed, 297 insertions(+), 150 deletions(-) delete mode 100644 VERSION (limited to 'frontend/src/components/panels/StreamsPane.js') diff --git a/Dockerfile b/Dockerfile index cf7730b..a9c8134 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,15 +3,16 @@ FROM ubuntu:20.04 AS BUILDSTAGE # Install tools and libraries RUN apt-get update && \ - DEBIAN_FRONTEND=noninteractive apt-get install -qq golang-1.14 pkg-config libpcap-dev libhyperscan-dev yarnpkg + DEBIAN_FRONTEND=noninteractive apt-get install -qq git golang-1.14 pkg-config libpcap-dev libhyperscan-dev yarnpkg COPY . /caronte WORKDIR /caronte RUN ln -sf ../lib/go-1.14/bin/go /usr/bin/go && \ + export VERSION=$(git describe --tags) && \ go mod download && \ - go build && \ + go build -ldflags "-X main.Version=$VERSION" && \ cd frontend && \ yarnpkg install && \ yarnpkg build --production=true && \ diff --git a/VERSION b/VERSION deleted file mode 100644 index bc1f22f..0000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -v0.20.10 \ No newline at end of file diff --git a/application_router.go b/application_router.go index 9fd7e3d..89b471b 100644 --- a/application_router.go +++ b/application_router.go @@ -65,7 +65,7 @@ func CreateApplicationRouter(applicationContext *ApplicationContext, applicationContext.SetAccounts(settings.Accounts) c.JSON(http.StatusAccepted, gin.H{}) - notificationController.Notify("setup", InsertNotification, gin.H{}) + notificationController.Notify("setup", gin.H{}) }) router.GET("/ws", func(c *gin.Context) { @@ -95,7 +95,7 @@ func CreateApplicationRouter(applicationContext *ApplicationContext, } else { response := UnorderedDocument{"id": id} success(c, response) - notificationController.Notify("rules.new", InsertNotification, response) + notificationController.Notify("rules.new", response) } }) @@ -134,7 +134,7 @@ func CreateApplicationRouter(applicationContext *ApplicationContext, notFound(c, UnorderedDocument{"id": id}) } else { success(c, rule) - notificationController.Notify("rules.edit", UpdateNotification, rule) + notificationController.Notify("rules.edit", rule) } }) @@ -156,7 +156,7 @@ func CreateApplicationRouter(applicationContext *ApplicationContext, } else { response := gin.H{"session": sessionID} c.JSON(http.StatusAccepted, response) - notificationController.Notify("pcap.upload", InsertNotification, response) + notificationController.Notify("pcap.upload", response) } }) @@ -190,7 +190,7 @@ func CreateApplicationRouter(applicationContext *ApplicationContext, } else { response := gin.H{"session": sessionID} c.JSON(http.StatusAccepted, response) - notificationController.Notify("pcap.file", InsertNotification, response) + notificationController.Notify("pcap.file", response) } }) @@ -227,7 +227,7 @@ func CreateApplicationRouter(applicationContext *ApplicationContext, session := gin.H{"session": sessionID} if cancelled := applicationContext.PcapImporter.CancelSession(sessionID); cancelled { c.JSON(http.StatusAccepted, session) - notificationController.Notify("sessions.delete", DeleteNotification, session) + notificationController.Notify("sessions.delete", session) } else { notFound(c, session) } @@ -288,7 +288,7 @@ func CreateApplicationRouter(applicationContext *ApplicationContext, if result { response := gin.H{"connection_id": c.Param("id"), "action": c.Param("action")} success(c, response) - notificationController.Notify("connections.action", UpdateNotification, response) + notificationController.Notify("connections.action", response) } else { notFound(c, gin.H{"connection": id}) } @@ -344,7 +344,7 @@ func CreateApplicationRouter(applicationContext *ApplicationContext, } if err := applicationContext.ServicesController.SetService(c, service); err == nil { success(c, service) - notificationController.Notify("services.edit", UpdateNotification, service) + notificationController.Notify("services.edit", service) } else { unprocessableEntity(c, err) } diff --git a/caronte.go b/caronte.go index d4265bc..2d24af6 100644 --- a/caronte.go +++ b/caronte.go @@ -21,9 +21,10 @@ import ( "flag" "fmt" log "github.com/sirupsen/logrus" - "io/ioutil" ) +var Version string + func main() { mongoHost := flag.String("mongo-host", "localhost", "address of MongoDB") mongoPort := flag.Int("mongo-port", 27017, "port of MongoDB") @@ -40,12 +41,10 @@ func main() { log.WithError(err).WithFields(logFields).Fatal("failed to connect to MongoDB") } - versionBytes, err := ioutil.ReadFile("VERSION") - if err != nil { - log.WithError(err).Fatal("failed to load version file") + if Version == "" { + Version = "undefined" } - - applicationContext, err := CreateApplicationContext(storage, string(versionBytes)) + applicationContext, err := CreateApplicationContext(storage, Version) if err != nil { log.WithError(err).WithFields(logFields).Fatal("failed to create application context") } diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico index 1dc499d..be9cec8 100644 Binary files a/frontend/public/favicon.ico and b/frontend/public/favicon.ico differ diff --git a/frontend/public/logo192.png b/frontend/public/logo192.png index 1dc499d..1969e1d 100644 Binary files a/frontend/public/logo192.png and b/frontend/public/logo192.png differ diff --git a/frontend/public/logo512.png b/frontend/public/logo512.png index 1dc499d..3afb127 100644 Binary files a/frontend/public/logo512.png and b/frontend/public/logo512.png differ diff --git a/frontend/src/components/App.js b/frontend/src/components/App.js index bf959c5..0f700db 100644 --- a/frontend/src/components/App.js +++ b/frontend/src/components/App.js @@ -31,7 +31,8 @@ class App extends Component { if (payload.event === "connected") { this.setState({ connected: true, - configured: payload.message["is_configured"] + configured: payload.message["is_configured"], + version: payload.message["version"] }); } }); @@ -50,7 +51,7 @@ class App extends Component { <> {this.state.connected ? - (this.state.configured ? : + (this.state.configured ? : this.setState({configured: true})}/>) : } diff --git a/frontend/src/components/Notifications.js b/frontend/src/components/Notifications.js index 1017a42..ad681a2 100644 --- a/frontend/src/components/Notifications.js +++ b/frontend/src/components/Notifications.js @@ -30,49 +30,84 @@ class Notifications extends Component { }; componentDidMount() { - dispatcher.register("notifications", notification => { + dispatcher.register("notifications", n => this.notificationHandler(n)); + } + + notificationHandler = (n) => { + switch (n.event) { + case "connected": + n.title = "connected"; + n.description = `number of active clients: ${n.message["connected_clients"]}`; + return this.pushNotification(n); + case "services.edit": + n.title = "services updated"; + n.description = `updated "${n.message["name"]}" on port ${n.message["port"]}`; + n.variant = "blue"; + return this.pushNotification(n); + case "rules.new": + n.title = "rules updated"; + n.description = `new rule added: ${n.message["name"]}`; + n.variant = "green"; + return this.pushNotification(n); + case "rules.edit": + n.title = "rules updated"; + n.description = `existing rule updated: ${n.message["name"]}`; + n.variant = "blue"; + return this.pushNotification(n); + default: + return; + } + }; + + pushNotification = (notification) => { + const notifications = this.state.notifications; + notifications.push(notification); + this.setState({notifications}); + setTimeout(() => { const notifications = this.state.notifications; - notifications.push(notification); + notification.open = true; this.setState({notifications}); - setTimeout(() => { - const notifications = this.state.notifications; - notification.open = true; - this.setState({notifications}); - }, 100); + }, 100); - const hideHandle = setTimeout(() => { - const notifications = _.without(this.state.notifications, notification); - const closedNotifications = this.state.closedNotifications.concat([notification]); - notification.closed = true; - this.setState({notifications, closedNotifications}); - }, 5000); + const hideHandle = setTimeout(() => { + const notifications = _.without(this.state.notifications, notification); + const closedNotifications = this.state.closedNotifications.concat([notification]); + notification.closed = true; + this.setState({notifications, closedNotifications}); + }, 5000); - const removeHandle = setTimeout(() => { - const closedNotifications = _.without(this.state.closedNotifications, notification); - this.setState({closedNotifications}); - }, 6000); + const removeHandle = setTimeout(() => { + const closedNotifications = _.without(this.state.closedNotifications, notification); + this.setState({closedNotifications}); + }, 6000); - notification.onClick = () => { - clearTimeout(hideHandle); - clearTimeout(removeHandle); - const notifications = _.without(this.state.notifications, notification); - this.setState({notifications}); - }; - }); - } + notification.onClick = () => { + clearTimeout(hideHandle); + clearTimeout(removeHandle); + const notifications = _.without(this.state.notifications, notification); + this.setState({notifications}); + }; + }; render() { return (
{ - this.state.closedNotifications.concat(this.state.notifications).map(n => -
-

{n.event}

- {JSON.stringify(n.message)} -
- ) + this.state.closedNotifications.concat(this.state.notifications).map(n => { + const notificationClassnames = { + "notification": true, + "notification-closed": n.closed, + "notification-open": n.open + }; + if (n.variant) { + notificationClassnames[`notification-${n.variant}`] = true; + } + return
+

{n.title}

+
{n.description}
+
; + }) }
diff --git a/frontend/src/components/Notifications.scss b/frontend/src/components/Notifications.scss index 324d0bb..98d228e 100644 --- a/frontend/src/components/Notifications.scss +++ b/frontend/src/components/Notifications.scss @@ -7,18 +7,15 @@ left: 30px; .notification { - overflow: hidden; width: 250px; margin: 10px 0; padding: 10px; + cursor: pointer; transition: all 1s ease; transform: translateX(-300px); - white-space: nowrap; - text-overflow: ellipsis; color: $color-green-light; border-left: 5px solid $color-green-dark; background-color: $color-green; - cursor: pointer; .notification-title { font-size: 0.9em; @@ -27,6 +24,11 @@ .notification-description { font-size: 0.8em; + overflow: hidden; + margin: 10px 0; + white-space: nowrap; + text-overflow: ellipsis; + color: $color-primary-4; } &.notification-open { @@ -37,5 +39,11 @@ transform: translateY(-50px); opacity: 0; } + + &.notification-blue { + color: $color-blue-light; + border-left: 5px solid $color-blue-dark; + background-color: $color-blue; + } } } diff --git a/frontend/src/components/Timeline.js b/frontend/src/components/Timeline.js index 7be42e0..615203f 100644 --- a/frontend/src/components/Timeline.js +++ b/frontend/src/components/Timeline.js @@ -35,6 +35,7 @@ import log from "../log"; import dispatcher from "../dispatcher"; const minutes = 60 * 1000; +const classNames = require('classnames'); class Timeline extends Component { @@ -70,6 +71,11 @@ class Timeline extends Component { this.loadServices().then(() => log.debug("Services reloaded after notification update")); } }); + + dispatcher.register("pulse_timeline", payload => { + this.setState({pulseTimeline: true}); + setTimeout(() => this.setState({pulseTimeline: false}), payload.duration); + }); } componentDidUpdate(prevProps, prevState, snapshot) { @@ -183,7 +189,7 @@ class Timeline extends Component { return (