aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEmiliano Ciavatta2020-10-16 17:06:05 +0000
committerEmiliano Ciavatta2020-10-16 17:06:05 +0000
commit56f70a72196c777f248038bb2e2e4099e6e1367d (patch)
tree714ad5aed8698dfffbb472b3fa74909acb8cdead
parent6204c99e69d1707a79c5e56685b47310106c60b0 (diff)
parent79b8b2fa3e8563c986da8baa3a761f2d4f0c6f47 (diff)
Merge branch 'develop'
-rw-r--r--Dockerfile5
-rw-r--r--README.md42
-rw-r--r--application_context.go45
-rw-r--r--application_context_test.go28
-rw-r--r--application_router.go145
-rw-r--r--application_router_test.go26
-rw-r--r--caronte.go34
-rw-r--r--caronte_test.go17
-rw-r--r--connection_handler.go49
-rw-r--r--connection_handler_test.go17
-rw-r--r--connection_streams_controller.go214
-rw-r--r--connections_controller.go88
-rw-r--r--frontend/.eslintrc.js216
-rw-r--r--frontend/package.json8
-rw-r--r--frontend/public/favicon.icobin34239 -> 12163 bytes
-rw-r--r--frontend/public/index.html2
-rw-r--r--frontend/public/logo128.pngbin0 -> 6498 bytes
-rw-r--r--frontend/public/logo192.pngbin34239 -> 0 bytes
-rw-r--r--frontend/public/logo512.pngbin34239 -> 26806 bytes
-rw-r--r--frontend/public/manifest.json8
-rw-r--r--frontend/src/backend.js48
-rw-r--r--frontend/src/components/App.js70
-rw-r--r--frontend/src/components/Connection.js134
-rw-r--r--frontend/src/components/ConnectionContent.js166
-rw-r--r--frontend/src/components/ConnectionMatchedRules.js29
-rw-r--r--frontend/src/components/Header.js101
-rw-r--r--frontend/src/components/Header.scss (renamed from frontend/src/views/Header.scss)11
-rw-r--r--frontend/src/components/Notifications.js131
-rw-r--r--frontend/src/components/Notifications.scss49
-rw-r--r--frontend/src/components/Timeline.js295
-rw-r--r--frontend/src/components/Timeline.scss27
-rw-r--r--frontend/src/components/dialogs/Filters.js85
-rw-r--r--frontend/src/components/dialogs/Filters.scss5
-rw-r--r--frontend/src/components/fields/ButtonField.js32
-rw-r--r--frontend/src/components/fields/ButtonField.scss7
-rw-r--r--frontend/src/components/fields/CheckField.js31
-rw-r--r--frontend/src/components/fields/ChoiceField.js27
-rw-r--r--frontend/src/components/fields/ChoiceField.scss8
-rw-r--r--frontend/src/components/fields/InputField.js37
-rw-r--r--frontend/src/components/fields/InputField.scss21
-rw-r--r--frontend/src/components/fields/TagField.js75
-rw-r--r--frontend/src/components/fields/TagField.scss157
-rw-r--r--frontend/src/components/fields/TextField.js27
-rw-r--r--frontend/src/components/fields/TextField.scss4
-rw-r--r--frontend/src/components/fields/common.scss4
-rw-r--r--frontend/src/components/fields/extensions/ColorField.js34
-rw-r--r--frontend/src/components/fields/extensions/NumericField.js23
-rw-r--r--frontend/src/components/filters/AdvancedFilters.js54
-rw-r--r--frontend/src/components/filters/BooleanConnectionsFilter.js83
-rw-r--r--frontend/src/components/filters/ExitSearchFilter.js57
-rw-r--r--frontend/src/components/filters/FiltersDefinitions.js90
-rw-r--r--frontend/src/components/filters/RulesConnectionsFilter.js115
-rw-r--r--frontend/src/components/filters/RulesConnectionsFilter.scss118
-rw-r--r--frontend/src/components/filters/StringConnectionsFilter.js114
-rw-r--r--frontend/src/components/objects/Connection.js114
-rw-r--r--frontend/src/components/objects/Connection.scss (renamed from frontend/src/components/Connection.scss)10
-rw-r--r--frontend/src/components/objects/ConnectionMatchedRules.js51
-rw-r--r--frontend/src/components/objects/ConnectionMatchedRules.scss (renamed from frontend/src/components/ConnectionMatchedRules.scss)2
-rw-r--r--frontend/src/components/objects/CopyLinkPopover.js54
-rw-r--r--frontend/src/components/objects/LinkPopover.js32
-rw-r--r--frontend/src/components/objects/LinkPopover.scss5
-rw-r--r--frontend/src/components/objects/MessageAction.js (renamed from frontend/src/components/MessageAction.js)36
-rw-r--r--frontend/src/components/objects/MessageAction.scss (renamed from frontend/src/components/MessageAction.scss)2
-rw-r--r--frontend/src/components/pages/ConfigurationPage.js (renamed from frontend/src/components/panels/ConfigurationPane.js)92
-rw-r--r--frontend/src/components/pages/ConfigurationPage.scss30
-rw-r--r--frontend/src/components/pages/MainPage.js76
-rw-r--r--frontend/src/components/pages/MainPage.scss25
-rw-r--r--frontend/src/components/pages/ServiceUnavailablePage.js34
-rw-r--r--frontend/src/components/pages/common.scss16
-rw-r--r--frontend/src/components/panels/ConfigurationPane.scss18
-rw-r--r--frontend/src/components/panels/ConnectionsPane.js310
-rw-r--r--frontend/src/components/panels/ConnectionsPane.scss41
-rw-r--r--frontend/src/components/panels/MainPane.js134
-rw-r--r--frontend/src/components/panels/MainPane.scss36
-rw-r--r--frontend/src/components/panels/PcapsPane.js (renamed from frontend/src/components/panels/PcapPane.js)130
-rw-r--r--frontend/src/components/panels/PcapsPane.scss (renamed from frontend/src/components/panels/PcapPane.scss)0
-rw-r--r--frontend/src/components/panels/RulesPane.js (renamed from frontend/src/components/panels/RulePane.js)254
-rw-r--r--frontend/src/components/panels/RulesPane.scss (renamed from frontend/src/components/panels/RulePane.scss)0
-rw-r--r--frontend/src/components/panels/SearchPane.js309
-rw-r--r--frontend/src/components/panels/SearchPane.scss52
-rw-r--r--frontend/src/components/panels/ServicesPane.js (renamed from frontend/src/components/panels/ServicePane.js)114
-rw-r--r--frontend/src/components/panels/ServicesPane.scss (renamed from frontend/src/components/panels/ServicePane.scss)0
-rw-r--r--frontend/src/components/panels/StreamsPane.js241
-rw-r--r--frontend/src/components/panels/StreamsPane.scss (renamed from frontend/src/components/ConnectionContent.scss)24
-rw-r--r--frontend/src/components/panels/common.scss12
-rw-r--r--frontend/src/dispatcher.js57
-rw-r--r--frontend/src/index.js44
-rw-r--r--frontend/src/index.scss16
-rw-r--r--frontend/src/log.js29
-rw-r--r--frontend/src/logo.svg8
-rw-r--r--frontend/src/notifications.js57
-rw-r--r--frontend/src/serviceWorker.js141
-rw-r--r--frontend/src/setupProxy.js24
-rw-r--r--frontend/src/setupTests.js5
-rw-r--r--frontend/src/utils.js67
-rw-r--r--frontend/src/validation.js16
-rw-r--r--frontend/src/views/App.js49
-rw-r--r--frontend/src/views/App.scss15
-rw-r--r--frontend/src/views/Connections.js201
-rw-r--r--frontend/src/views/Connections.scss37
-rw-r--r--frontend/src/views/Filters.js99
-rw-r--r--frontend/src/views/Footer.js19
-rw-r--r--frontend/src/views/Footer.scss13
-rw-r--r--frontend/src/views/Header.js92
-rw-r--r--frontend/yarn.lock481
-rw-r--r--go.mod4
-rw-r--r--go.sum14
-rw-r--r--notification_controller.go178
-rw-r--r--parsers/http_request_parser.go60
-rw-r--r--parsers/http_response_parser.go29
-rw-r--r--parsers/parser.go21
-rw-r--r--parsers/parser_utils.go17
-rw-r--r--pcap_importer.go63
-rw-r--r--pcap_importer_test.go18
-rw-r--r--resources_controller.go108
-rw-r--r--rules_manager.go60
-rw-r--r--rules_manager_test.go32
-rwxr-xr-xscripts/generate_ping.py2
-rw-r--r--search_controller.go211
-rw-r--r--services_controller.go17
-rw-r--r--statistics_controller.go103
-rw-r--r--storage.go87
-rw-r--r--storage_test.go17
-rw-r--r--stream_handler.go19
-rw-r--r--stream_handler_test.go17
-rw-r--r--utils.go33
126 files changed, 5792 insertions, 2255 deletions
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/README.md b/README.md
index 0251be1..75158e2 100644
--- a/README.md
+++ b/README.md
@@ -13,23 +13,23 @@ The patterns can be defined as regex or using protocol specific rules.
The connection flows are saved into a database and can be visualized with the web application. REST API are also provided.
## Features
-- immediate installation with docker-compose
-- no configuration file, settings can be changed via GUI or API
-- the pcaps to be analyzed can be loaded via `curl`, either locally or remotely, or via the GUI
- - it is also possible to download the pcaps from the GUI and see all the analysis statistics for each pcap
-- rules can be created to identify connections that contain certain strings
- - pattern matching is done through regular expressions (regex)
- - regex in UTF-8 and Unicode format are also supported
- - it is possible to add an additional filter to the connections identified through pattern matching by type of connection
-- the connections can be labeled by type of service, identified by the port number
- - each service can be assigned a different color
-- it is possible to filter connections by addresses, ports, dimensions, time, duration, matched rules
-- supports both IPv4 and IPv6 addresses
- - if more addresses are assigned to the vulnerable machine to be defended, a CIDR address can be used
-- the detected HTTP connections are automatically reconstructed
- - HTTP requests can be replicated through `curl`, `fetch` and `python requests`
- - compressed HTTP responses (gzip/deflate) are automatically decompressed
-- it is possible to export and view the content of connections in various formats, including hex and base64
+- immediate installation with docker-compose
+- no configuration file, settings can be changed via GUI or API
+- the pcaps to be analyzed can be loaded via `curl`, either locally or remotely, or via the GUI
+ - it is also possible to download the pcaps from the GUI and see all the analysis statistics for each pcap
+- rules can be created to identify connections that contain certain strings
+ - pattern matching is done through regular expressions (regex)
+ - regex in UTF-8 and Unicode format are also supported
+ - it is possible to add an additional filter to the connections identified through pattern matching by type of connection
+- the connections can be labeled by type of service, identified by the port number
+ - each service can be assigned a different color
+- it is possible to filter connections by addresses, ports, dimensions, time, duration, matched rules
+- supports both IPv4 and IPv6 addresses
+ - if more addresses are assigned to the vulnerable machine to be defended, a CIDR address can be used
+- the detected HTTP connections are automatically reconstructed
+ - HTTP requests can be replicated through `curl`, `fetch` and `python requests`
+ - compressed HTTP responses (gzip/deflate) are automatically decompressed
+- it is possible to export and view the content of connections in various formats, including hex and base64
## Installation
There are two ways to install Caronte:
@@ -77,16 +77,16 @@ The backend, written in Go language, it is designed as a service. It exposes RES
## Screenshots
Below there are some screenshots showing the main features of the tool.
-#### Viewing the contents of a connection
+### Viewing the contents of a connection
![Connection Content](frontend/screenshots/connection_content.png)
-#### Loading pcaps and analysis details
+### Loading pcaps and analysis details
![Connection Content](frontend/screenshots/pcaps.png)
-#### Creating new pattern matching rules
+### Creating new pattern matching rules
![Connection Content](frontend/screenshots/rules.png)
-#### Creating or editing services
+### Creating or editing services
![Connection Content](frontend/screenshots/services.png)
## License
diff --git a/application_context.go b/application_context.go
index e4be74d..0410b88 100644
--- a/application_context.go
+++ b/application_context.go
@@ -1,3 +1,20 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
package main
import (
@@ -20,10 +37,14 @@ type ApplicationContext struct {
ConnectionsController ConnectionsController
ServicesController *ServicesController
ConnectionStreamsController ConnectionStreamsController
+ SearchController *SearchController
+ StatisticsController StatisticsController
+ NotificationController *NotificationController
IsConfigured bool
+ Version string
}
-func CreateApplicationContext(storage Storage) (*ApplicationContext, error) {
+func CreateApplicationContext(storage Storage, version string) (*ApplicationContext, error) {
var configWrapper struct {
Config Config
}
@@ -44,18 +65,18 @@ func CreateApplicationContext(storage Storage) (*ApplicationContext, error) {
}
applicationContext := &ApplicationContext{
- Storage: storage,
- Config: configWrapper.Config,
- Accounts: accountsWrapper.Accounts,
+ Storage: storage,
+ Config: configWrapper.Config,
+ Accounts: accountsWrapper.Accounts,
+ Version: version,
}
- applicationContext.configure()
return applicationContext, nil
}
func (sm *ApplicationContext) SetConfig(config Config) {
sm.Config = config
- sm.configure()
+ sm.Configure()
var upsertResults interface{}
if _, err := sm.Storage.Update(Settings).Upsert(&upsertResults).
Filter(OrderedDocument{{"_id", "config"}}).One(UnorderedDocument{"config": config}); err != nil {
@@ -72,7 +93,11 @@ func (sm *ApplicationContext) SetAccounts(accounts gin.Accounts) {
}
}
-func (sm *ApplicationContext) configure() {
+func (sm *ApplicationContext) SetNotificationController(notificationController *NotificationController) {
+ sm.NotificationController = notificationController
+}
+
+func (sm *ApplicationContext) Configure() {
if sm.IsConfigured {
return
}
@@ -89,9 +114,11 @@ func (sm *ApplicationContext) configure() {
log.WithError(err).Panic("failed to create a RulesManager")
}
sm.RulesManager = rulesManager
- sm.PcapImporter = NewPcapImporter(sm.Storage, *serverNet, sm.RulesManager)
+ sm.PcapImporter = NewPcapImporter(sm.Storage, *serverNet, sm.RulesManager, sm.NotificationController)
sm.ServicesController = NewServicesController(sm.Storage)
- sm.ConnectionsController = NewConnectionsController(sm.Storage, sm.ServicesController)
+ sm.SearchController = NewSearchController(sm.Storage)
+ sm.ConnectionsController = NewConnectionsController(sm.Storage, sm.SearchController, sm.ServicesController)
sm.ConnectionStreamsController = NewConnectionStreamsController(sm.Storage)
+ sm.StatisticsController = NewStatisticsController(sm.Storage)
sm.IsConfigured = true
}
diff --git a/application_context_test.go b/application_context_test.go
index eed0fd6..11d1ed4 100644
--- a/application_context_test.go
+++ b/application_context_test.go
@@ -1,3 +1,20 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
package main
import (
@@ -10,7 +27,7 @@ func TestCreateApplicationContext(t *testing.T) {
wrapper := NewTestStorageWrapper(t)
wrapper.AddCollection(Settings)
- appContext, err := CreateApplicationContext(wrapper.Storage)
+ appContext, err := CreateApplicationContext(wrapper.Storage, "test")
assert.NoError(t, err)
assert.False(t, appContext.IsConfigured)
assert.Zero(t, appContext.Config)
@@ -18,6 +35,10 @@ func TestCreateApplicationContext(t *testing.T) {
assert.Nil(t, appContext.PcapImporter)
assert.Nil(t, appContext.RulesManager)
+ notificationController := NewNotificationController(appContext)
+ appContext.SetNotificationController(notificationController)
+ assert.Equal(t, notificationController, appContext.NotificationController)
+
config := Config{
ServerAddress: "10.10.10.10",
FlagRegex: "FLAG{test}",
@@ -39,13 +60,16 @@ func TestCreateApplicationContext(t *testing.T) {
appContext.SetConfig(config)
appContext.SetAccounts(accounts)
- checkAppContext, err := CreateApplicationContext(wrapper.Storage)
+ checkAppContext, err := CreateApplicationContext(wrapper.Storage, "test")
assert.NoError(t, err)
+ checkAppContext.SetNotificationController(notificationController)
+ checkAppContext.Configure()
assert.True(t, checkAppContext.IsConfigured)
assert.Equal(t, checkAppContext.Config, config)
assert.Equal(t, checkAppContext.Accounts, accounts)
assert.NotNil(t, checkAppContext.PcapImporter)
assert.NotNil(t, checkAppContext.RulesManager)
+ assert.Equal(t, notificationController, appContext.NotificationController)
wrapper.Destroy(t)
}
diff --git a/application_router.go b/application_router.go
index 501956b..4048dc5 100644
--- a/application_router.go
+++ b/application_router.go
@@ -1,3 +1,20 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
package main
import (
@@ -13,7 +30,8 @@ import (
"time"
)
-func CreateApplicationRouter(applicationContext *ApplicationContext) *gin.Engine {
+func CreateApplicationRouter(applicationContext *ApplicationContext,
+ notificationController *NotificationController, resourcesController *ResourcesController) *gin.Engine {
router := gin.New()
router.Use(gin.Logger())
router.Use(gin.Recovery())
@@ -21,7 +39,7 @@ func CreateApplicationRouter(applicationContext *ApplicationContext) *gin.Engine
router.Use(static.Serve("/", static.LocalFile("./frontend/build", true)))
- for _, path := range []string{"/connections/:id", "/pcaps", "/rules", "/services", "/config"} {
+ for _, path := range []string{"/connections/:id", "/pcaps", "/rules", "/services", "/config", "/searches"} {
router.GET(path, func(c *gin.Context) {
c.File("./frontend/build/index.html")
})
@@ -47,6 +65,13 @@ func CreateApplicationRouter(applicationContext *ApplicationContext) *gin.Engine
applicationContext.SetAccounts(settings.Accounts)
c.JSON(http.StatusAccepted, gin.H{})
+ notificationController.Notify("setup", gin.H{})
+ })
+
+ router.GET("/ws", func(c *gin.Context) {
+ if err := notificationController.NotificationHandler(c.Writer, c.Request); err != nil {
+ serverError(c, err)
+ }
})
api := router.Group("/api")
@@ -68,7 +93,9 @@ func CreateApplicationRouter(applicationContext *ApplicationContext) *gin.Engine
if id, err := applicationContext.RulesManager.AddRule(c, rule); err != nil {
unprocessableEntity(c, err)
} else {
- success(c, UnorderedDocument{"id": id})
+ response := UnorderedDocument{"id": id}
+ success(c, response)
+ notificationController.Notify("rules.new", response)
}
})
@@ -107,6 +134,7 @@ func CreateApplicationRouter(applicationContext *ApplicationContext) *gin.Engine
notFound(c, UnorderedDocument{"id": id})
} else {
success(c, rule)
+ notificationController.Notify("rules.edit", rule)
}
})
@@ -119,14 +147,16 @@ func CreateApplicationRouter(applicationContext *ApplicationContext) *gin.Engine
flushAllValue, isPresent := c.GetPostForm("flush_all")
flushAll := isPresent && strings.ToLower(flushAllValue) == "true"
fileName := fmt.Sprintf("%v-%s", time.Now().UnixNano(), fileHeader.Filename)
- if err := c.SaveUploadedFile(fileHeader, ProcessingPcapsBasePath + fileName); err != nil {
+ if err := c.SaveUploadedFile(fileHeader, ProcessingPcapsBasePath+fileName); err != nil {
log.WithError(err).Panic("failed to save uploaded file")
}
if sessionID, err := applicationContext.PcapImporter.ImportPcap(fileName, flushAll); err != nil {
unprocessableEntity(c, err)
} else {
- c.JSON(http.StatusAccepted, gin.H{"session": sessionID})
+ response := gin.H{"session": sessionID}
+ c.JSON(http.StatusAccepted, response)
+ notificationController.Notify("pcap.upload", response)
}
})
@@ -147,7 +177,7 @@ func CreateApplicationRouter(applicationContext *ApplicationContext) *gin.Engine
}
fileName := fmt.Sprintf("%v-%s", time.Now().UnixNano(), filepath.Base(request.File))
- if err := CopyFile(ProcessingPcapsBasePath + fileName, request.File); err != nil {
+ if err := CopyFile(ProcessingPcapsBasePath+fileName, request.File); err != nil {
log.WithError(err).Panic("failed to copy pcap file")
}
if sessionID, err := applicationContext.PcapImporter.ImportPcap(fileName, request.FlushAll); err != nil {
@@ -158,7 +188,9 @@ func CreateApplicationRouter(applicationContext *ApplicationContext) *gin.Engine
}
unprocessableEntity(c, err)
} else {
- c.JSON(http.StatusAccepted, gin.H{"session": sessionID})
+ response := gin.H{"session": sessionID}
+ c.JSON(http.StatusAccepted, response)
+ notificationController.Notify("pcap.file", response)
}
})
@@ -179,9 +211,9 @@ func CreateApplicationRouter(applicationContext *ApplicationContext) *gin.Engine
sessionID := c.Param("id")
if _, isPresent := applicationContext.PcapImporter.GetSession(sessionID); isPresent {
if FileExists(PcapsBasePath + sessionID + ".pcap") {
- c.FileAttachment(PcapsBasePath + sessionID + ".pcap", sessionID[:16] + ".pcap")
+ c.FileAttachment(PcapsBasePath+sessionID+".pcap", sessionID[:16]+".pcap")
} else if FileExists(PcapsBasePath + sessionID + ".pcapng") {
- c.FileAttachment(PcapsBasePath + sessionID + ".pcapng", sessionID[:16] + ".pcapng")
+ c.FileAttachment(PcapsBasePath+sessionID+".pcapng", sessionID[:16]+".pcapng")
} else {
log.WithField("sessionID", sessionID).Panic("pcap file not exists")
}
@@ -195,6 +227,7 @@ func CreateApplicationRouter(applicationContext *ApplicationContext) *gin.Engine
session := gin.H{"session": sessionID}
if cancelled := applicationContext.PcapImporter.CancelSession(sessionID); cancelled {
c.JSON(http.StatusAccepted, session)
+ notificationController.Notify("sessions.delete", session)
} else {
notFound(c, session)
}
@@ -253,24 +286,89 @@ func CreateApplicationRouter(applicationContext *ApplicationContext) *gin.Engine
}
if result {
- c.Status(http.StatusAccepted)
+ response := gin.H{"connection_id": c.Param("id"), "action": c.Param("action")}
+ success(c, response)
+ notificationController.Notify("connections.action", response)
} else {
notFound(c, gin.H{"connection": id})
}
})
+ api.GET("/searches", func(c *gin.Context) {
+ success(c, applicationContext.SearchController.GetPerformedSearches())
+ })
+
+ api.POST("/searches/perform", func(c *gin.Context) {
+ var options SearchOptions
+
+ if err := c.ShouldBindJSON(&options); err != nil {
+ badRequest(c, err)
+ return
+ }
+
+ // stupid checks because validator library is a shit
+ var badContentError error
+ if options.TextSearch.isZero() == options.RegexSearch.isZero() {
+ badContentError = errors.New("specify either 'text_search' or 'regex_search'")
+ }
+ if !options.TextSearch.isZero() {
+ if (options.TextSearch.Terms == nil) == (options.TextSearch.ExactPhrase == "") {
+ badContentError = errors.New("specify either 'terms' or 'exact_phrase'")
+ }
+ if (options.TextSearch.Terms == nil) && (options.TextSearch.ExcludedTerms != nil) {
+ badContentError = errors.New("'excluded_terms' must be specified only with 'terms'")
+ }
+ }
+ if !options.RegexSearch.isZero() {
+ if (options.RegexSearch.Pattern == "") == (options.RegexSearch.NotPattern == "") {
+ badContentError = errors.New("specify either 'pattern' or 'not_pattern'")
+ }
+ }
+
+ if badContentError != nil {
+ badRequest(c, badContentError)
+ return
+ }
+
+ success(c, applicationContext.SearchController.PerformSearch(c, options))
+ })
+
api.GET("/streams/:id", func(c *gin.Context) {
id, err := RowIDFromHex(c.Param("id"))
if err != nil {
badRequest(c, err)
return
}
- var format QueryFormat
+ var format GetMessageFormat
if err := c.ShouldBindQuery(&format); err != nil {
badRequest(c, err)
return
}
- success(c, applicationContext.ConnectionStreamsController.GetConnectionPayload(c, id, format))
+
+ if messages, found := applicationContext.ConnectionStreamsController.GetConnectionMessages(c, id, format); !found {
+ notFound(c, gin.H{"connection": id})
+ } else {
+ success(c, messages)
+ }
+ })
+
+ api.GET("/streams/:id/download", func(c *gin.Context) {
+ id, err := RowIDFromHex(c.Param("id"))
+ if err != nil {
+ badRequest(c, err)
+ return
+ }
+ var format DownloadMessageFormat
+ if err := c.ShouldBindQuery(&format); err != nil {
+ badRequest(c, err)
+ return
+ }
+
+ if blob, found := applicationContext.ConnectionStreamsController.DownloadConnectionMessages(c, id, format); !found {
+ notFound(c, gin.H{"connection": id})
+ } else {
+ c.String(http.StatusOK, blob)
+ }
})
api.GET("/services", func(c *gin.Context) {
@@ -285,13 +383,30 @@ func CreateApplicationRouter(applicationContext *ApplicationContext) *gin.Engine
}
if err := applicationContext.ServicesController.SetService(c, service); err == nil {
success(c, service)
+ notificationController.Notify("services.edit", service)
} else {
unprocessableEntity(c, err)
}
})
- }
+ api.GET("/statistics", func(c *gin.Context) {
+ var filter StatisticsFilter
+ if err := c.ShouldBindQuery(&filter); err != nil {
+ badRequest(c, err)
+ return
+ }
+ success(c, applicationContext.StatisticsController.GetStatistics(c, filter))
+ })
+
+ api.GET("/resources/system", func(c *gin.Context) {
+ success(c, resourcesController.GetSystemStats(c))
+ })
+
+ api.GET("/resources/process", func(c *gin.Context) {
+ success(c, resourcesController.GetProcessStats(c))
+ })
+ }
return router
}
@@ -352,3 +467,7 @@ func unprocessableEntity(c *gin.Context, err error) {
func notFound(c *gin.Context, obj interface{}) {
c.JSON(http.StatusNotFound, obj)
}
+
+func serverError(c *gin.Context, err error) {
+ c.JSON(http.StatusInternalServerError, UnorderedDocument{"result": "error", "error": err.Error()})
+}
diff --git a/application_router_test.go b/application_router_test.go
index 4225ab9..27c3651 100644
--- a/application_router_test.go
+++ b/application_router_test.go
@@ -1,3 +1,20 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
package main
import (
@@ -92,7 +109,7 @@ func TestRulesApi(t *testing.T) {
var rules []Rule
assert.Equal(t, http.StatusOK, w.Code)
assert.NoError(t, json.Unmarshal(w.Body.Bytes(), &rules))
- assert.Len(t, rules, 3)
+ assert.Len(t, rules, 4)
toolkit.wrapper.Destroy(t)
}
@@ -148,10 +165,13 @@ func NewRouterTestToolkit(t *testing.T, withSetup bool) *RouterTestToolkit {
wrapper := NewTestStorageWrapper(t)
wrapper.AddCollection(Settings)
- appContext, err := CreateApplicationContext(wrapper.Storage)
+ appContext, err := CreateApplicationContext(wrapper.Storage, "test")
require.NoError(t, err)
gin.SetMode(gin.ReleaseMode)
- router := CreateApplicationRouter(appContext)
+ notificationController := NewNotificationController(appContext)
+ go notificationController.Run()
+ resourcesController := NewResourcesController(notificationController)
+ router := CreateApplicationRouter(appContext, notificationController, resourcesController)
toolkit := RouterTestToolkit{
appContext: appContext,
diff --git a/caronte.go b/caronte.go
index 098642c..95f00ef 100644
--- a/caronte.go
+++ b/caronte.go
@@ -1,3 +1,20 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
package main
import (
@@ -6,6 +23,8 @@ import (
log "github.com/sirupsen/logrus"
)
+var Version string
+
func main() {
mongoHost := flag.String("mongo-host", "localhost", "address of MongoDB")
mongoPort := flag.Int("mongo-port", 27017, "port of MongoDB")
@@ -22,12 +41,23 @@ func main() {
log.WithError(err).WithFields(logFields).Fatal("failed to connect to MongoDB")
}
- applicationContext, err := CreateApplicationContext(storage)
+ if Version == "" {
+ Version = "undefined"
+ }
+ applicationContext, err := CreateApplicationContext(storage, Version)
if err != nil {
log.WithError(err).WithFields(logFields).Fatal("failed to create application context")
}
- applicationRouter := CreateApplicationRouter(applicationContext)
+ notificationController := NewNotificationController(applicationContext)
+ go notificationController.Run()
+ applicationContext.SetNotificationController(notificationController)
+
+ resourcesController := NewResourcesController(notificationController)
+ go resourcesController.Run()
+
+ applicationContext.Configure()
+ applicationRouter := CreateApplicationRouter(applicationContext, notificationController, resourcesController)
if applicationRouter.Run(fmt.Sprintf("%s:%v", *bindAddress, *bindPort)) != nil {
log.WithError(err).WithFields(logFields).Fatal("failed to create the server")
}
diff --git a/caronte_test.go b/caronte_test.go
index 12ec50f..8935ea3 100644
--- a/caronte_test.go
+++ b/caronte_test.go
@@ -1,3 +1,20 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
package main
import (
diff --git a/connection_handler.go b/connection_handler.go
index ffe4fac..4e92ccf 100644
--- a/connection_handler.go
+++ b/connection_handler.go
@@ -1,7 +1,25 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
package main
import (
"encoding/binary"
+ "fmt"
"github.com/flier/gohs/hyperscan"
"github.com/google/gopacket"
"github.com/google/gopacket/tcpassembly"
@@ -225,6 +243,37 @@ func (ch *connectionHandlerImpl) Complete(handler *StreamHandler) {
log.WithError(err).WithField("connection", connection).Error("failed to update all connections streams")
}
}
+
+ ch.UpdateStatistics(connection)
+}
+
+func (ch *connectionHandlerImpl) UpdateStatistics(connection Connection) {
+ rangeStart := connection.StartedAt.Unix() / 60 // group statistic records by minutes
+ duration := connection.ClosedAt.Sub(connection.StartedAt)
+ // if one of the two parts doesn't close connection, the duration is +infinity or -infinity
+ if duration.Hours() > 1 || duration.Hours() < -1 {
+ duration = 0
+ }
+ servicePort := connection.DestinationPort
+
+ updateDocument := UnorderedDocument{
+ fmt.Sprintf("connections_per_service.%d", servicePort): 1,
+ fmt.Sprintf("client_bytes_per_service.%d", servicePort): connection.ClientBytes,
+ fmt.Sprintf("server_bytes_per_service.%d", servicePort): connection.ServerBytes,
+ fmt.Sprintf("total_bytes_per_service.%d", servicePort): connection.ClientBytes + connection.ServerBytes,
+ fmt.Sprintf("duration_per_service.%d", servicePort): duration.Milliseconds(),
+ }
+
+ for _, ruleID := range connection.MatchedRules {
+ updateDocument[fmt.Sprintf("matched_rules.%s", ruleID.Hex())] = 1
+ }
+
+ var results interface{}
+ if _, err := ch.Storage().Update(Statistics).Upsert(&results).
+ Filter(OrderedDocument{{"_id", time.Unix(rangeStart*60, 0)}}).
+ OneComplex(UnorderedDocument{"$inc": updateDocument}); err != nil {
+ log.WithError(err).WithField("connection", connection).Error("failed to update connection statistics")
+ }
}
func (ch *connectionHandlerImpl) Storage() Storage {
diff --git a/connection_handler_test.go b/connection_handler_test.go
index 0bee0ac..d980041 100644
--- a/connection_handler_test.go
+++ b/connection_handler_test.go
@@ -1,3 +1,20 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
package main
import (
diff --git a/connection_streams_controller.go b/connection_streams_controller.go
index 9d73b0e..038c2c5 100644
--- a/connection_streams_controller.go
+++ b/connection_streams_controller.go
@@ -1,16 +1,37 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
package main
import (
"bytes"
"context"
+ "fmt"
"github.com/eciavatta/caronte/parsers"
log "github.com/sirupsen/logrus"
+ "strings"
"time"
)
-const InitialPayloadsSize = 1024
-const DefaultQueryFormatLimit = 8024
-const InitialRegexSlicesCount = 8
+const (
+ initialMessagesSize = 1024
+ initialRegexSlicesCount = 8
+ pwntoolsMaxServerBytes = 20
+)
type ConnectionStream struct {
ID RowID `bson:"_id"`
@@ -18,6 +39,7 @@ type ConnectionStream struct {
FromClient bool `bson:"from_client"`
DocumentIndex int `bson:"document_index"`
Payload []byte `bson:"payload"`
+ PayloadString string `bson:"payload_string"`
BlocksIndexes []int `bson:"blocks_indexes"`
BlocksTimestamps []time.Time `bson:"blocks_timestamps"`
BlocksLoss []bool `bson:"blocks_loss"`
@@ -26,7 +48,7 @@ type ConnectionStream struct {
type PatternSlice [2]uint64
-type Payload struct {
+type Message struct {
FromClient bool `json:"from_client"`
Content string `json:"content"`
Metadata parsers.Metadata `json:"metadata"`
@@ -42,10 +64,13 @@ type RegexSlice struct {
To uint64 `json:"to"`
}
-type QueryFormat struct {
+type GetMessageFormat struct {
+ Format string `form:"format"`
+}
+
+type DownloadMessageFormat struct {
Format string `form:"format"`
- Skip uint64 `form:"skip"`
- Limit uint64 `form:"limit"`
+ Type string `form:"type"`
}
type ConnectionStreamsController struct {
@@ -58,15 +83,16 @@ func NewConnectionStreamsController(storage Storage) ConnectionStreamsController
}
}
-func (csc ConnectionStreamsController) GetConnectionPayload(c context.Context, connectionID RowID,
- format QueryFormat) []*Payload {
- payloads := make([]*Payload, 0, InitialPayloadsSize)
- var clientIndex, serverIndex, globalIndex uint64
-
- if format.Limit <= 0 {
- format.Limit = DefaultQueryFormatLimit
+func (csc ConnectionStreamsController) GetConnectionMessages(c context.Context, connectionID RowID,
+ format GetMessageFormat) ([]*Message, bool) {
+ connection := csc.getConnection(c, connectionID)
+ if connection.ID.IsZero() {
+ return nil, false
}
+ messages := make([]*Message, 0, initialMessagesSize)
+ var clientIndex, serverIndex uint64
+
var clientBlocksIndex, serverBlocksIndex int
var clientDocumentIndex, serverDocumentIndex int
clientStream := csc.getConnectionStream(c, connectionID, true, clientDocumentIndex)
@@ -79,8 +105,8 @@ func (csc ConnectionStreamsController) GetConnectionPayload(c context.Context, c
return serverBlocksIndex < len(serverStream.BlocksIndexes)
}
- var payload *Payload
- payloadsBuffer := make([]*Payload, 0, 16)
+ var message *Message
+ messagesBuffer := make([]*Message, 0, 16)
contentChunkBuffer := new(bytes.Buffer)
var lastContentSlice []byte
var sideChanged, lastClient, lastServer bool
@@ -97,7 +123,7 @@ func (csc ConnectionStreamsController) GetConnectionPayload(c context.Context, c
}
size := uint64(end - start)
- payload = &Payload{
+ message = &Message{
FromClient: true,
Content: DecodeBytes(clientStream.Payload[start:end], format.Format),
Index: start,
@@ -106,7 +132,6 @@ func (csc ConnectionStreamsController) GetConnectionPayload(c context.Context, c
RegexMatches: findMatchesBetween(clientStream.PatternMatches, clientIndex, clientIndex+size),
}
clientIndex += size
- globalIndex += size
clientBlocksIndex++
lastContentSlice = clientStream.Payload[start:end]
@@ -121,7 +146,7 @@ func (csc ConnectionStreamsController) GetConnectionPayload(c context.Context, c
}
size := uint64(end - start)
- payload = &Payload{
+ message = &Message{
FromClient: false,
Content: DecodeBytes(serverStream.Payload[start:end], format.Format),
Index: start,
@@ -130,7 +155,6 @@ func (csc ConnectionStreamsController) GetConnectionPayload(c context.Context, c
RegexMatches: findMatchesBetween(serverStream.PatternMatches, serverIndex, serverIndex+size),
}
serverIndex += size
- globalIndex += size
serverBlocksIndex++
lastContentSlice = serverStream.Payload[start:end]
@@ -140,49 +164,150 @@ func (csc ConnectionStreamsController) GetConnectionPayload(c context.Context, c
if !hasClientBlocks() {
clientDocumentIndex++
clientBlocksIndex = 0
+ clientIndex = 0
clientStream = csc.getConnectionStream(c, connectionID, true, clientDocumentIndex)
}
if !hasServerBlocks() {
serverDocumentIndex++
serverBlocksIndex = 0
+ serverIndex = 0
serverStream = csc.getConnectionStream(c, connectionID, false, serverDocumentIndex)
}
updateMetadata := func() {
metadata := parsers.Parse(contentChunkBuffer.Bytes())
var isMetadataContinuation bool
- for _, elem := range payloadsBuffer {
+ for _, elem := range messagesBuffer {
elem.Metadata = metadata
elem.IsMetadataContinuation = isMetadataContinuation
isMetadataContinuation = true
}
- payloadsBuffer = payloadsBuffer[:0]
+ messagesBuffer = messagesBuffer[:0]
contentChunkBuffer.Reset()
}
if sideChanged {
updateMetadata()
}
- payloadsBuffer = append(payloadsBuffer, payload)
+ messagesBuffer = append(messagesBuffer, message)
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)
+ messages = append(messages, message)
+ }
+
+ return messages, true
+}
+
+func (csc ConnectionStreamsController) DownloadConnectionMessages(c context.Context, connectionID RowID,
+ format DownloadMessageFormat) (string, bool) {
+ connection := csc.getConnection(c, connectionID)
+ if connection.ID.IsZero() {
+ return "", false
+ }
+
+ var sb strings.Builder
+ includeClient, includeServer := format.Type != "only_server", format.Type != "only_client"
+ isPwntools := format.Type == "pwntools"
+
+ var clientBlocksIndex, serverBlocksIndex int
+ var clientDocumentIndex, serverDocumentIndex int
+ var clientStream ConnectionStream
+ if includeClient {
+ clientStream = csc.getConnectionStream(c, connectionID, true, clientDocumentIndex)
+ }
+ var serverStream ConnectionStream
+ if includeServer {
+ serverStream = csc.getConnectionStream(c, connectionID, false, serverDocumentIndex)
+ }
+
+ hasClientBlocks := func() bool {
+ return clientBlocksIndex < len(clientStream.BlocksIndexes)
+ }
+ hasServerBlocks := func() bool {
+ return serverBlocksIndex < len(serverStream.BlocksIndexes)
+ }
+
+ if isPwntools {
+ if format.Format == "base32" || format.Format == "base64" {
+ sb.WriteString("import base64\n")
}
- 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
+ sb.WriteString("from pwn import *\n\n")
+ sb.WriteString(fmt.Sprintf("p = remote('%s', %d)\n", connection.DestinationIP, connection.DestinationPort))
+ }
+
+ lastIsClient, lastIsServer := true, true
+ for !clientStream.ID.IsZero() || !serverStream.ID.IsZero() {
+ if hasClientBlocks() && (!hasServerBlocks() || // next payload is from client
+ clientStream.BlocksTimestamps[clientBlocksIndex].UnixNano() <=
+ serverStream.BlocksTimestamps[serverBlocksIndex].UnixNano()) {
+ start := clientStream.BlocksIndexes[clientBlocksIndex]
+ end := 0
+ if clientBlocksIndex < len(clientStream.BlocksIndexes)-1 {
+ end = clientStream.BlocksIndexes[clientBlocksIndex+1]
+ } else {
+ end = len(clientStream.Payload)
+ }
+
+ if !lastIsClient {
+ sb.WriteString("\n")
+ }
+ lastIsClient = true
+ lastIsServer = false
+ if isPwntools {
+ sb.WriteString(decodePwntools(clientStream.Payload[start:end], true, format.Format))
+ } else {
+ sb.WriteString(DecodeBytes(clientStream.Payload[start:end], format.Format))
+ }
+ clientBlocksIndex++
+ } else { // next payload is from server
+ start := serverStream.BlocksIndexes[serverBlocksIndex]
+ end := 0
+ if serverBlocksIndex < len(serverStream.BlocksIndexes)-1 {
+ end = serverStream.BlocksIndexes[serverBlocksIndex+1]
+ } else {
+ end = len(serverStream.Payload)
+ }
+
+ if !lastIsServer {
+ sb.WriteString("\n")
+ }
+ lastIsClient = false
+ lastIsServer = true
+ if isPwntools {
+ sb.WriteString(decodePwntools(serverStream.Payload[start:end], false, format.Format))
+ } else {
+ sb.WriteString(DecodeBytes(serverStream.Payload[start:end], format.Format))
+ }
+ serverBlocksIndex++
+ }
+
+ if includeClient && !hasClientBlocks() {
+ clientDocumentIndex++
+ clientBlocksIndex = 0
+ clientStream = csc.getConnectionStream(c, connectionID, true, clientDocumentIndex)
+ }
+ if includeServer && !hasServerBlocks() {
+ serverDocumentIndex++
+ serverBlocksIndex = 0
+ serverStream = csc.getConnectionStream(c, connectionID, false, serverDocumentIndex)
}
}
- return payloads
+ return sb.String(), true
+}
+
+func (csc ConnectionStreamsController) getConnection(c context.Context, connectionID RowID) Connection {
+ var connection Connection
+ if err := csc.storage.Find(Connections).Context(c).Filter(OrderedDocument{{"_id", connectionID}}).
+ First(&connection); err != nil {
+ log.WithError(err).WithField("id", connectionID).Panic("failed to get connection")
+ }
+ return connection
}
func (csc ConnectionStreamsController) getConnectionStream(c context.Context, connectionID RowID, fromClient bool,
@@ -199,14 +324,13 @@ func (csc ConnectionStreamsController) getConnectionStream(c context.Context, co
}
func findMatchesBetween(patternMatches map[uint][]PatternSlice, from, to uint64) []RegexSlice {
- regexSlices := make([]RegexSlice, 0, InitialRegexSlicesCount)
+ regexSlices := make([]RegexSlice, 0, initialRegexSlicesCount)
for _, slices := range patternMatches {
for _, slice := range slices {
if from > slice[1] || to <= slice[0] {
continue
}
- log.Info(slice[0], slice[1], from, to)
var start, end uint64
if from > slice[0] {
start = 0
@@ -225,3 +349,27 @@ func findMatchesBetween(patternMatches map[uint][]PatternSlice, from, to uint64)
}
return regexSlices
}
+
+func decodePwntools(payload []byte, isClient bool, format string) string {
+ if !isClient && len(payload) > pwntoolsMaxServerBytes {
+ payload = payload[len(payload)-pwntoolsMaxServerBytes:]
+ }
+
+ var content string
+ switch format {
+ case "hex":
+ content = fmt.Sprintf("bytes.fromhex('%s')", DecodeBytes(payload, format))
+ case "base32":
+ content = fmt.Sprintf("base64.b32decode('%s')", DecodeBytes(payload, format))
+ case "base64":
+ content = fmt.Sprintf("base64.b64decode('%s')", DecodeBytes(payload, format))
+ default:
+ content = fmt.Sprintf("'%s'", strings.Replace(DecodeBytes(payload, "ascii"), "'", "\\'", -1))
+ }
+
+ if isClient {
+ return fmt.Sprintf("p.send(%s)\n", content)
+ }
+
+ return fmt.Sprintf("p.recvuntil(%s)\n", content)
+}
diff --git a/connections_controller.go b/connections_controller.go
index 2773506..924cb53 100644
--- a/connections_controller.go
+++ b/connections_controller.go
@@ -1,3 +1,20 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
package main
import (
@@ -31,48 +48,54 @@ type Connection struct {
}
type ConnectionsFilter struct {
- From string `form:"from" binding:"omitempty,hexadecimal,len=24"`
- To string `form:"to" binding:"omitempty,hexadecimal,len=24"`
- ServicePort uint16 `form:"service_port"`
- ClientAddress string `form:"client_address" binding:"omitempty,ip"`
- ClientPort uint16 `form:"client_port"`
- MinDuration uint `form:"min_duration"`
- MaxDuration uint `form:"max_duration" binding:"omitempty,gtefield=MinDuration"`
- MinBytes uint `form:"min_bytes"`
- MaxBytes uint `form:"max_bytes" binding:"omitempty,gtefield=MinBytes"`
- StartedAfter int64 `form:"started_after" `
- StartedBefore int64 `form:"started_before" binding:"omitempty,gtefield=StartedAfter"`
- ClosedAfter int64 `form:"closed_after" `
- ClosedBefore int64 `form:"closed_before" binding:"omitempty,gtefield=ClosedAfter"`
- Hidden bool `form:"hidden"`
- Marked bool `form:"marked"`
- MatchedRules []string `form:"matched_rules" binding:"dive,hexadecimal,len=24"`
- Limit int64 `form:"limit"`
+ From string `form:"from" binding:"omitempty,hexadecimal,len=24"`
+ To string `form:"to" binding:"omitempty,hexadecimal,len=24"`
+ ServicePort uint16 `form:"service_port"`
+ ClientAddress string `form:"client_address" binding:"omitempty,ip"`
+ ClientPort uint16 `form:"client_port"`
+ MinDuration uint `form:"min_duration"`
+ MaxDuration uint `form:"max_duration" binding:"omitempty,gtefield=MinDuration"`
+ MinBytes uint `form:"min_bytes"`
+ MaxBytes uint `form:"max_bytes" binding:"omitempty,gtefield=MinBytes"`
+ StartedAfter int64 `form:"started_after" `
+ StartedBefore int64 `form:"started_before" binding:"omitempty,gtefield=StartedAfter"`
+ ClosedAfter int64 `form:"closed_after" `
+ ClosedBefore int64 `form:"closed_before" binding:"omitempty,gtefield=ClosedAfter"`
+ Hidden bool `form:"hidden"`
+ Marked bool `form:"marked"`
+ MatchedRules []string `form:"matched_rules" binding:"dive,hexadecimal,len=24"`
+ PerformedSearch string `form:"performed_search" binding:"omitempty,hexadecimal,len=24"`
+ Limit int64 `form:"limit"`
}
type ConnectionsController struct {
- storage Storage
+ storage Storage
+ searchController *SearchController
servicesController *ServicesController
}
-func NewConnectionsController(storage Storage, servicesController *ServicesController) ConnectionsController {
+func NewConnectionsController(storage Storage, searchesController *SearchController,
+ servicesController *ServicesController) ConnectionsController {
return ConnectionsController{
- storage: storage,
+ storage: storage,
+ searchController: searchesController,
servicesController: servicesController,
}
}
func (cc ConnectionsController) GetConnections(c context.Context, filter ConnectionsFilter) []Connection {
var connections []Connection
- query := cc.storage.Find(Connections).Context(c).Sort("_id", false)
+ query := cc.storage.Find(Connections).Context(c)
from, _ := RowIDFromHex(filter.From)
if !from.IsZero() {
- query = query.Filter(OrderedDocument{{"_id", UnorderedDocument{"$lt": from}}})
+ query = query.Filter(OrderedDocument{{"_id", UnorderedDocument{"$lte": from}}})
}
to, _ := RowIDFromHex(filter.To)
if !to.IsZero() {
- query = query.Filter(OrderedDocument{{"_id", UnorderedDocument{"$gt": to}}})
+ query = query.Filter(OrderedDocument{{"_id", UnorderedDocument{"$gte": to}}})
+ } else {
+ query = query.Sort("_id", false)
}
if filter.ServicePort > 0 {
query = query.Filter(OrderedDocument{{"port_dst", filter.ServicePort}})
@@ -125,6 +148,13 @@ func (cc ConnectionsController) GetConnections(c context.Context, filter Connect
query = query.Filter(OrderedDocument{{"matched_rules", UnorderedDocument{"$all": matchedRules}}})
}
+ performedSearchID, _ := RowIDFromHex(filter.PerformedSearch)
+ if !performedSearchID.IsZero() {
+ performedSearch := cc.searchController.GetPerformedSearch(performedSearchID)
+ if !performedSearch.ID.IsZero() && len(performedSearch.AffectedConnections) > 0 {
+ query = query.Filter(OrderedDocument{{"_id", UnorderedDocument{"$in": performedSearch.AffectedConnections}}})
+ }
+ }
if filter.Limit > 0 && filter.Limit <= MaxQueryLimit {
query = query.Limit(filter.Limit)
} else {
@@ -146,6 +176,10 @@ func (cc ConnectionsController) GetConnections(c context.Context, filter Connect
}
}
+ if !to.IsZero() {
+ connections = reverseConnections(connections)
+ }
+
return connections
}
@@ -178,3 +212,11 @@ func (cc ConnectionsController) setProperty(c context.Context, id RowID, propert
}
return updated
}
+
+func reverseConnections(connections []Connection) []Connection {
+ for i := 0; i < len(connections)/2; i++ {
+ j := len(connections) - i - 1
+ connections[i], connections[j] = connections[j], connections[i]
+ }
+ return connections
+}
diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js
deleted file mode 100644
index 16ff228..0000000
--- a/frontend/.eslintrc.js
+++ /dev/null
@@ -1,216 +0,0 @@
-const OFF = 0, WARN = 1, ERROR = 2;
-
-module.exports = exports = {
- "env": {
- "es6": true
- },
-
- "ecmaFeatures": {
- // env=es6 doesn't include modules, which we are using
- "modules": true
- },
-
- "extends": "eslint:recommended",
-
- "rules": {
- // Possible Errors (overrides from recommended set)
- "no-extra-parens": ERROR,
- "no-unexpected-multiline": ERROR,
- // All JSDoc comments must be valid
- "valid-jsdoc": [ ERROR, {
- "requireReturn": false,
- "requireReturnDescription": false,
- "requireParamDescription": true,
- "prefer": {
- "return": "returns"
- }
- }],
-
- // Best Practices
-
- // Allowed a getter without setter, but all setters require getters
- "accessor-pairs": [ ERROR, {
- "getWithoutSet": false,
- "setWithoutGet": true
- }],
- "block-scoped-var": WARN,
- "consistent-return": ERROR,
- "curly": ERROR,
- "default-case": WARN,
- // the dot goes with the property when doing multiline
- "dot-location": [ WARN, "property" ],
- "dot-notation": WARN,
- "eqeqeq": [ ERROR, "smart" ],
- "guard-for-in": WARN,
- "no-alert": ERROR,
- "no-caller": ERROR,
- "no-case-declarations": WARN,
- "no-div-regex": WARN,
- "no-else-return": WARN,
- "no-empty-label": WARN,
- "no-empty-pattern": WARN,
- "no-eq-null": WARN,
- "no-eval": ERROR,
- "no-extend-native": ERROR,
- "no-extra-bind": WARN,
- "no-floating-decimal": WARN,
- "no-implicit-coercion": [ WARN, {
- "boolean": true,
- "number": true,
- "string": true
- }],
- "no-implied-eval": ERROR,
- "no-invalid-this": ERROR,
- "no-iterator": ERROR,
- "no-labels": WARN,
- "no-lone-blocks": WARN,
- "no-loop-func": ERROR,
- "no-magic-numbers": WARN,
- "no-multi-spaces": ERROR,
- "no-multi-str": WARN,
- "no-native-reassign": ERROR,
- "no-new-func": ERROR,
- "no-new-wrappers": ERROR,
- "no-new": ERROR,
- "no-octal-escape": ERROR,
- "no-param-reassign": ERROR,
- "no-process-env": WARN,
- "no-proto": ERROR,
- "no-redeclare": ERROR,
- "no-return-assign": ERROR,
- "no-script-url": ERROR,
- "no-self-compare": ERROR,
- "no-throw-literal": ERROR,
- "no-unused-expressions": ERROR,
- "no-useless-call": ERROR,
- "no-useless-concat": ERROR,
- "no-void": WARN,
- // Produce warnings when something is commented as TODO or FIXME
- "no-warning-comments": [ WARN, {
- "terms": [ "TODO", "FIXME" ],
- "location": "start"
- }],
- "no-with": WARN,
- "radix": WARN,
- "vars-on-top": ERROR,
- // Enforces the style of wrapped functions
- "wrap-iife": [ ERROR, "outside" ],
- "yoda": ERROR,
-
- // Strict Mode - for ES6, never use strict.
- "strict": [ ERROR, "never" ],
-
- // Variables
- "init-declarations": [ ERROR, "always" ],
- "no-catch-shadow": WARN,
- "no-delete-var": ERROR,
- "no-label-var": ERROR,
- "no-shadow-restricted-names": ERROR,
- "no-shadow": WARN,
- // We require all vars to be initialized (see init-declarations)
- // If we NEED a var to be initialized to undefined, it needs to be explicit
- "no-undef-init": OFF,
- "no-undef": ERROR,
- "no-undefined": OFF,
- "no-unused-vars": WARN,
- // Disallow hoisting - let & const don't allow hoisting anyhow
- "no-use-before-define": ERROR,
-
- // Node.js and CommonJS
- "callback-return": [ WARN, [ "callback", "next" ]],
- "global-require": ERROR,
- "handle-callback-err": WARN,
- "no-mixed-requires": WARN,
- "no-new-require": ERROR,
- // Use path.concat instead
- "no-path-concat": ERROR,
- "no-process-exit": ERROR,
- "no-restricted-modules": OFF,
- "no-sync": WARN,
-
- // ECMAScript 6 support
- "arrow-body-style": [ ERROR, "always" ],
- "arrow-parens": [ ERROR, "always" ],
- "arrow-spacing": [ ERROR, { "before": true, "after": true }],
- "constructor-super": ERROR,
- "generator-star-spacing": [ ERROR, "before" ],
- "no-arrow-condition": ERROR,
- "no-class-assign": ERROR,
- "no-const-assign": ERROR,
- "no-dupe-class-members": ERROR,
- "no-this-before-super": ERROR,
- "no-var": WARN,
- "object-shorthand": [ WARN, "never" ],
- "prefer-arrow-callback": WARN,
- "prefer-spread": WARN,
- "prefer-template": WARN,
- "require-yield": ERROR,
-
- // Stylistic - everything here is a warning because of style.
- "array-bracket-spacing": [ WARN, "always" ],
- "block-spacing": [ WARN, "always" ],
- "brace-style": [ WARN, "1tbs", { "allowSingleLine": false } ],
- "camelcase": WARN,
- "comma-spacing": [ WARN, { "before": false, "after": true } ],
- "comma-style": [ WARN, "last" ],
- "computed-property-spacing": [ WARN, "never" ],
- "consistent-this": [ WARN, "self" ],
- "eol-last": WARN,
- "func-names": WARN,
- "func-style": [ WARN, "declaration" ],
- "id-length": [ WARN, { "min": 2, "max": 32 } ],
- "indent": [ WARN, 4 ],
- "jsx-quotes": [ WARN, "prefer-double" ],
- "linebreak-style": [ WARN, "unix" ],
- "lines-around-comment": [ WARN, { "beforeBlockComment": true } ],
- "max-depth": [ WARN, 8 ],
- "max-len": [ WARN, 132 ],
- "max-nested-callbacks": [ WARN, 8 ],
- "max-params": [ WARN, 8 ],
- "new-cap": WARN,
- "new-parens": WARN,
- "no-array-constructor": WARN,
- "no-bitwise": OFF,
- "no-continue": OFF,
- "no-inline-comments": OFF,
- "no-lonely-if": WARN,
- "no-mixed-spaces-and-tabs": WARN,
- "no-multiple-empty-lines": WARN,
- "no-negated-condition": OFF,
- "no-nested-ternary": WARN,
- "no-new-object": WARN,
- "no-plusplus": OFF,
- "no-spaced-func": WARN,
- "no-ternary": OFF,
- "no-trailing-spaces": WARN,
- "no-underscore-dangle": WARN,
- "no-unneeded-ternary": WARN,
- "object-curly-spacing": [ WARN, "always" ],
- "one-var": OFF,
- "operator-assignment": [ WARN, "never" ],
- "operator-linebreak": [ WARN, "after" ],
- "padded-blocks": [ WARN, "never" ],
- "quote-props": [ WARN, "consistent-as-needed" ],
- "quotes": [ WARN, "single" ],
- "require-jsdoc": [ WARN, {
- "require": {
- "FunctionDeclaration": true,
- "MethodDefinition": true,
- "ClassDeclaration": false
- }
- }],
- "semi-spacing": [ WARN, { "before": false, "after": true }],
- "semi": [ ERROR, "always" ],
- "sort-vars": OFF,
- "space-after-keywords": [ WARN, "always" ],
- "space-before-blocks": [ WARN, "always" ],
- "space-before-function-paren": [ WARN, "never" ],
- "space-before-keywords": [ WARN, "always" ],
- "space-in-parens": [ WARN, "never" ],
- "space-infix-ops": [ WARN, { "int32Hint": true } ],
- "space-return-throw-case": ERROR,
- "space-unary-ops": ERROR,
- "spaced-comment": [ WARN, "always" ],
- "wrap-regex": WARN
- }
-};
diff --git a/frontend/package.json b/frontend/package.json
index b13a7ea..b3ad03a 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -12,17 +12,22 @@
"bootstrap": "^4.4.1",
"bs-custom-file-input": "^1.3.4",
"classnames": "^2.2.6",
+ "dompurify": "^2.1.1",
"eslint-config-react-app": "^5.2.1",
+ "http-proxy-middleware": "^1.0.5",
"lodash": "^4.17.20",
"node-sass": "^4.14.0",
+ "pondjs": "^0.9.0",
"react": "^16.13.1",
"react-bootstrap": "^1.0.1",
"react-dom": "^16.13.1",
"react-input-mask": "^3.0.0-alpha.2",
+ "react-json-view": "^1.19.1",
"react-router": "^5.1.2",
"react-router-dom": "^5.1.2",
"react-scripts": "3.4.1",
"react-tag-autocomplete": "^6.0.0-beta.6",
+ "react-timeseries-charts": "^0.16.1",
"typed.js": "^2.0.11"
},
"scripts": {
@@ -45,6 +50,5 @@
"last 1 firefox version",
"last 1 safari version"
]
- },
- "proxy": "http://localhost:3333"
+ }
}
diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico
index 1dc499d..be9cec8 100644
--- a/frontend/public/favicon.ico
+++ b/frontend/public/favicon.ico
Binary files differ
diff --git a/frontend/public/index.html b/frontend/public/index.html
index 5cc974e..1c98f08 100644
--- a/frontend/public/index.html
+++ b/frontend/public/index.html
@@ -8,7 +8,7 @@
<meta name="description" content="A tool to analyze the network flow during attack/defence capture the flag events" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
- <title>Caronte</title>
+ <title>caronte:~/$</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
diff --git a/frontend/public/logo128.png b/frontend/public/logo128.png
new file mode 100644
index 0000000..1969e1d
--- /dev/null
+++ b/frontend/public/logo128.png
Binary files differ
diff --git a/frontend/public/logo192.png b/frontend/public/logo192.png
deleted file mode 100644
index 1dc499d..0000000
--- a/frontend/public/logo192.png
+++ /dev/null
Binary files differ
diff --git a/frontend/public/logo512.png b/frontend/public/logo512.png
index 1dc499d..3afb127 100644
--- a/frontend/public/logo512.png
+++ b/frontend/public/logo512.png
Binary files differ
diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json
index 0409a59..32674ce 100644
--- a/frontend/public/manifest.json
+++ b/frontend/public/manifest.json
@@ -1,6 +1,6 @@
{
- "short_name": "Caronte",
- "name": "Caronte",
+ "short_name": "caronte",
+ "name": "caronte",
"icons": [
{
"src": "favicon.ico",
@@ -8,9 +8,9 @@
"type": "image/x-icon"
},
{
- "src": "logo192.png",
+ "src": "logo128.png",
"type": "image/png",
- "sizes": "192x192"
+ "sizes": "128x128"
},
{
"src": "logo512.png",
diff --git a/frontend/src/backend.js b/frontend/src/backend.js
index 72ee9dd..dc5089f 100644
--- a/frontend/src/backend.js
+++ b/frontend/src/backend.js
@@ -1,7 +1,23 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
async function json(method, url, data, json, headers) {
const options = {
- method: method,
+ method,
body: json != null ? JSON.stringify(json) : data,
mode: "cors",
cache: "no-cache",
@@ -26,9 +42,33 @@ async function json(method, url, data, json, headers) {
}
}
+async function download(url, headers) {
+
+ const options = {
+ mode: "cors",
+ cache: "no-cache",
+ credentials: "same-origin",
+ headers: headers || {},
+ redirect: "follow",
+ referrerPolicy: "no-referrer",
+ };
+ const response = await fetch(url, options);
+ const result = {
+ statusCode: response.status,
+ status: `${response.status} ${response.statusText}`,
+ blob: await response.blob()
+ };
+
+ if (response.status >= 200 && response.status < 300) {
+ return result;
+ } else {
+ return Promise.reject(result);
+ }
+}
+
const backend = {
get: (url = "", headers = null) =>
- json("GET", url, null,null, headers),
+ json("GET", url, null, null, headers),
post: (url = "", data = null, headers = null) =>
json("POST", url, null, data, headers),
put: (url = "", data = null, headers = null) =>
@@ -36,7 +76,9 @@ const backend = {
delete: (url = "", data = null, headers = null) =>
json("DELETE", url, null, data, headers),
postFile: (url = "", data = null, headers = {}) =>
- json("POST", url, data, null, headers)
+ json("POST", url, data, null, headers),
+ download: (url = "", headers = null) =>
+ download(url, headers)
};
export default backend;
diff --git a/frontend/src/components/App.js b/frontend/src/components/App.js
new file mode 100644
index 0000000..96083cd
--- /dev/null
+++ b/frontend/src/components/App.js
@@ -0,0 +1,70 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import React, {Component} from "react";
+import {BrowserRouter as Router} from "react-router-dom";
+import dispatcher from "../dispatcher";
+import Notifications from "./Notifications";
+import ConfigurationPage from "./pages/ConfigurationPage";
+import MainPage from "./pages/MainPage";
+import ServiceUnavailablePage from "./pages/ServiceUnavailablePage";
+
+class App extends Component {
+
+ state = {};
+
+ componentDidMount() {
+ dispatcher.register("notifications", this.handleNotifications);
+
+ setInterval(() => {
+ if (document.title.endsWith("❚")) {
+ document.title = document.title.slice(0, -1);
+ } else {
+ document.title += "❚";
+ }
+ }, 500);
+ }
+
+ componentWillUnmount() {
+ dispatcher.unregister(this.handleNotifications);
+ }
+
+ handleNotifications = (payload) => {
+ if (payload.event === "connected") {
+ this.setState({
+ connected: true,
+ configured: payload.message["is_configured"],
+ version: payload.message["version"]
+ });
+ }
+ };
+
+ render() {
+ return (
+ <Router>
+ <Notifications/>
+ {this.state.connected ?
+ (this.state.configured ? <MainPage version={this.state.version}/> :
+ <ConfigurationPage onConfigured={() => this.setState({configured: true})}/>) :
+ <ServiceUnavailablePage/>
+ }
+ </Router>
+ );
+ }
+}
+
+export default App;
diff --git a/frontend/src/components/Connection.js b/frontend/src/components/Connection.js
deleted file mode 100644
index 44f9f18..0000000
--- a/frontend/src/components/Connection.js
+++ /dev/null
@@ -1,134 +0,0 @@
-import React, {Component} from 'react';
-import './Connection.scss';
-import {Form, OverlayTrigger, Popover} from "react-bootstrap";
-import backend from "../backend";
-import {dateTimeToTime, durationBetween, formatSize} from "../utils";
-import ButtonField from "./fields/ButtonField";
-
-const classNames = require('classnames');
-
-class Connection extends Component {
-
- constructor(props) {
- super(props);
- this.state = {
- update: false,
- copiedMessage: false
- };
-
- this.copyTextarea = React.createRef();
- this.handleAction = this.handleAction.bind(this);
- }
-
- handleAction(name) {
- if (name === "hide") {
- const enabled = !this.props.data.hidden;
- backend.post(`/api/connections/${this.props.data.id}/${enabled ? "hide" : "show"}`)
- .then(_ => {
- this.props.onEnabled(!enabled);
- this.setState({update: true});
- });
- }
- if (name === "mark") {
- const marked = this.props.data.marked;
- backend.post(`/api/connections/${this.props.data.id}/${marked ? "unmark" : "mark"}`)
- .then(_ => {
- this.props.onMarked(!marked);
- this.setState({update: true});
- });
- }
- if (name === "copy") {
- this.copyTextarea.current.select();
- document.execCommand('copy');
- this.setState({copiedMessage: true});
- setTimeout(() => this.setState({copiedMessage: false}), 3000);
- }
- }
-
- render() {
- let conn = this.props.data;
- let serviceName = "/dev/null";
- let serviceColor = "#0F192E";
- if (conn.service.port !== 0) {
- serviceName = conn.service.name;
- serviceColor = conn.service.color;
- }
- let startedAt = new Date(conn.started_at);
- let closedAt = new Date(conn.closed_at);
- let processedAt = new Date(conn.processed_at);
- let timeInfo = <div>
- <span>Started at {startedAt.toLocaleDateString() + " " + startedAt.toLocaleTimeString()}</span><br/>
- <span>Processed at {processedAt.toLocaleDateString() + " " + processedAt.toLocaleTimeString()}</span><br/>
- <span>Closed at {closedAt.toLocaleDateString() + " " + closedAt.toLocaleTimeString()}</span>
- </div>;
-
- const popoverFor = function (name, content) {
- return <Popover id={`popover-${name}-${conn.id}`} className="connection-popover">
- <Popover.Content>
- {content}
- </Popover.Content>
- </Popover>;
- };
-
- const commentPopoverContent = <div>
- <span>Click to <strong>{conn.comment.length > 0 ? "edit" : "add"}</strong> comment</span>
- {conn.comment && <Form.Control as="textarea" readOnly={true} rows={2} defaultValue={conn.comment}/>}
- </div>;
-
- const copyPopoverContent = <div>
- {this.state.copiedMessage ? <span><strong>Copied!</strong></span> :
- <span>Click to <strong>copy</strong> the connection id</span>}
- <Form.Control as="textarea" readOnly={true} rows={1} defaultValue={conn.id} ref={this.copyTextarea}/>
- </div>;
-
- return (
- <tr className={classNames("connection", {"connection-selected": this.props.selected},
- {"has-matched-rules": conn.matched_rules.length > 0})}>
- <td>
- <span className="connection-service">
- <ButtonField small fullSpan color={serviceColor} name={serviceName}
- onClick={() => this.props.addServicePortFilter(conn.port_dst)} />
- </span>
- </td>
- <td className="clickable" onClick={this.props.onSelected}>{conn.ip_src}</td>
- <td className="clickable" onClick={this.props.onSelected}>{conn.port_src}</td>
- <td className="clickable" onClick={this.props.onSelected}>{conn.ip_dst}</td>
- <td className="clickable" onClick={this.props.onSelected}>{conn.port_dst}</td>
- <td className="clickable" onClick={this.props.onSelected}>{dateTimeToTime(conn.started_at)}</td>
- <td className="clickable" onClick={this.props.onSelected}>
- <OverlayTrigger trigger={["focus", "hover"]} placement="right"
- overlay={popoverFor("duration", timeInfo)}>
- <span className="test-tooltip">{durationBetween(startedAt, closedAt)}</span>
- </OverlayTrigger>
- </td>
- <td className="clickable" onClick={this.props.onSelected}>{formatSize(conn.client_bytes)}</td>
- <td className="clickable" onClick={this.props.onSelected}>{formatSize(conn.server_bytes)}</td>
- <td>
- {/*<OverlayTrigger trigger={["focus", "hover"]} placement="right"*/}
- {/* overlay={popoverFor("hide", <span>Hide this connection from the list</span>)}>*/}
- {/* <span className={"connection-icon" + (conn.hidden ? " icon-enabled" : "")}*/}
- {/* onClick={() => this.handleAction("hide")}>%</span>*/}
- {/*</OverlayTrigger>*/}
- <OverlayTrigger trigger={["focus", "hover"]} placement="right"
- overlay={popoverFor("hide", <span>Mark this connection</span>)}>
- <span className={"connection-icon" + (conn.marked ? " icon-enabled" : "")}
- onClick={() => this.handleAction("mark")}>!!</span>
- </OverlayTrigger>
- <OverlayTrigger trigger={["focus", "hover"]} placement="right"
- overlay={popoverFor("comment", commentPopoverContent)}>
- <span className={"connection-icon" + (conn.comment ? " icon-enabled" : "")}
- onClick={() => this.handleAction("comment")}>@</span>
- </OverlayTrigger>
- <OverlayTrigger trigger={["focus", "hover"]} placement="right"
- overlay={popoverFor("copy", copyPopoverContent)}>
- <span className="connection-icon"
- onClick={() => this.handleAction("copy")}>#</span>
- </OverlayTrigger>
- </td>
- </tr>
- );
- }
-
-}
-
-export default Connection;
diff --git a/frontend/src/components/ConnectionContent.js b/frontend/src/components/ConnectionContent.js
deleted file mode 100644
index ccaec0b..0000000
--- a/frontend/src/components/ConnectionContent.js
+++ /dev/null
@@ -1,166 +0,0 @@
-import React, {Component} from 'react';
-import './ConnectionContent.scss';
-import {Row} from 'react-bootstrap';
-import MessageAction from "./MessageAction";
-import backend from "../backend";
-import ButtonField from "./fields/ButtonField";
-import ChoiceField from "./fields/ChoiceField";
-
-const classNames = require('classnames');
-
-class ConnectionContent extends Component {
-
- constructor(props) {
- super(props);
- this.state = {
- loading: false,
- connectionContent: null,
- format: "default",
- tryParse: true,
- messageActionDialog: null
- };
-
- this.validFormats = ["default", "hex", "hexdump", "base32", "base64", "ascii", "binary", "decimal", "octal"];
- this.setFormat = this.setFormat.bind(this);
- }
-
- componentDidMount() {
- if (this.props.connection != null) {
- this.loadStream();
- }
- }
-
- componentDidUpdate(prevProps, prevState, snapshot) {
- if (this.props.connection != null && (
- this.props.connection !== prevProps.connection || this.state.format !== prevState.format)) {
- this.loadStream();
- }
- }
-
- loadStream = () => {
- this.setState({loading: true});
- // TODO: limit workaround.
- backend.get(`/api/streams/${this.props.connection.id}?format=${this.state.format}&limit=999999`).then(res => {
- this.setState({
- connectionContent: res.json,
- loading: false
- });
- });
- };
-
- 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 <span style={{"fontSize": "12px"}}>**already parsed in previous messages**</span>;
- }
-
- let unrollMap = (obj) => obj == null ? null : Object.entries(obj).map(([key, value]) =>
- <p key={key}><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"
- rel="noopener noreferrer">{m.host}{m.url}</a></u></i>;
- return <span className="type-http-request">
- <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={{"marginBottom": "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]) =>
- <ButtonField small key={actionName + "_button"} name={actionName} onClick={() => {
- this.setState({
- messageActionDialog: <MessageAction actionName={actionName} actionValue={actionValue}
- onHide={() => this.setState({messageActionDialog: null})}/>
- });
- }} />
- );
- }
-
- render() {
- let content = this.state.connectionContent;
-
- if (content == null) {
- return <div>select a connection to view</div>;
- }
-
- let payload = content.map((c, i) =>
- <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.tryParse && this.state.format === "default" ? this.tryParseConnectionMessage(c) : c.content}
- </div>
- </div>
- );
-
- return (
- <div className="connection-content">
- <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="header-actions col-auto">
- <ChoiceField name="format" inline small onlyName
- keys={["default", "hex", "hexdump", "base32", "base64", "ascii", "binary", "decimal", "octal"]}
- values={["plain", "hex", "hexdump", "base32", "base64", "ascii", "binary", "decimal", "octal"]}
- onChange={this.setFormat} value={this.state.value} />
-
- <ChoiceField name="view_as" inline small onlyName keys={["default"]} values={["default"]} />
-
- <ChoiceField name="download_as" inline small onlyName
- keys={["nl_separated", "only_client", "only_server"]}
- values={["nl_separated", "only_client", "only_server"]} />
- </div>
- </Row>
- </div>
-
- <pre>{payload}</pre>
- {this.state.messageActionDialog}
- </div>
- );
- }
-
-}
-
-
-export default ConnectionContent;
diff --git a/frontend/src/components/ConnectionMatchedRules.js b/frontend/src/components/ConnectionMatchedRules.js
deleted file mode 100644
index 21f2a92..0000000
--- a/frontend/src/components/ConnectionMatchedRules.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import React, {Component} from 'react';
-import './ConnectionMatchedRules.scss';
-import ButtonField from "./fields/ButtonField";
-
-class ConnectionMatchedRules extends Component {
-
- constructor(props) {
- super(props);
- this.state = {
- };
- }
-
- render() {
- const matchedRules = this.props.matchedRules.map(mr => {
- const rule = this.props.rules.find(r => r.id === mr);
- return <ButtonField key={mr} onClick={() => this.props.addMatchedRulesFilter(rule.id)} name={rule.name}
- color={rule.color} small />;
- });
-
- return (
- <tr className="connection-matches">
- <td className="row-label">matched_rules:</td>
- <td className="rule-buttons" colSpan={9}>{matchedRules}</td>
- </tr>
- );
- }
-}
-
-export default ConnectionMatchedRules;
diff --git a/frontend/src/components/Header.js b/frontend/src/components/Header.js
new file mode 100644
index 0000000..c46d768
--- /dev/null
+++ b/frontend/src/components/Header.js
@@ -0,0 +1,101 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import React, {Component} from "react";
+import {Link, withRouter} from "react-router-dom";
+import Typed from "typed.js";
+import {cleanNumber, validatePort} from "../utils";
+import ButtonField from "./fields/ButtonField";
+import AdvancedFilters from "./filters/AdvancedFilters";
+import BooleanConnectionsFilter from "./filters/BooleanConnectionsFilter";
+import ExitSearchFilter from "./filters/ExitSearchFilter";
+import RulesConnectionsFilter from "./filters/RulesConnectionsFilter";
+import StringConnectionsFilter from "./filters/StringConnectionsFilter";
+import "./Header.scss";
+
+const classNames = require("classnames");
+
+class Header extends Component {
+
+ componentDidMount() {
+ const options = {
+ strings: ["caronte$ "],
+ typeSpeed: 50,
+ cursorChar: "❚"
+ };
+ this.typed = new Typed(this.el, options);
+ }
+
+ componentWillUnmount() {
+ this.typed.destroy();
+ }
+
+ render() {
+ return (
+ <header className="header container-fluid">
+ <div className="row">
+ <div className={classNames({"col-auto": this.props.configured, "col": !this.props.configured})}>
+ <h1 className="header-title type-wrap">
+ <Link to="/">
+ <span style={{whiteSpace: "pre"}} ref={(el) => {
+ this.el = el;
+ }}/>
+ </Link>
+ </h1>
+ </div>
+
+ {this.props.configured && <div className="col-auto">
+ <div className="filters-bar">
+ <StringConnectionsFilter filterName="service_port"
+ defaultFilterValue="all_ports"
+ replaceFunc={cleanNumber}
+ validateFunc={validatePort}
+ key="service_port_filter"
+ width={200} small inline/>
+ <RulesConnectionsFilter/>
+ <BooleanConnectionsFilter filterName={"marked"}/>
+ <ExitSearchFilter/>
+ <AdvancedFilters onClick={this.props.onOpenFilters}/>
+ </div>
+ </div>}
+
+ {this.props.configured && <div className="col">
+ <div className="header-buttons">
+ <Link to={"/searches" + this.props.location.search}>
+ <ButtonField variant="pink" name="searches" bordered/>
+ </Link>
+ <Link to={"/pcaps" + this.props.location.search}>
+ <ButtonField variant="purple" name="pcaps" bordered/>
+ </Link>
+ <Link to={"/rules" + this.props.location.search}>
+ <ButtonField variant="deep-purple" name="rules" bordered/>
+ </Link>
+ <Link to={"/services" + this.props.location.search}>
+ <ButtonField variant="indigo" name="services" bordered/>
+ </Link>
+ <Link to={"/config" + this.props.location.search}>
+ <ButtonField variant="blue" name="config" bordered/>
+ </Link>
+ </div>
+ </div>}
+ </div>
+ </header>
+ );
+ }
+}
+
+export default withRouter(Header);
diff --git a/frontend/src/views/Header.scss b/frontend/src/components/Header.scss
index 0711159..fff28e6 100644
--- a/frontend/src/views/Header.scss
+++ b/frontend/src/components/Header.scss
@@ -1,4 +1,4 @@
-@import "../colors.scss";
+@import "../colors";
.header {
height: 80px;
@@ -26,9 +26,16 @@
.filters-bar {
padding: 3px 0;
- .filter {
+ .filter,
+ .button-field {
display: inline-block;
margin-right: 10px;
}
+
+ .button-field button {
+ font-weight: 400;
+ padding: 7px 10px;
+ border-radius: 5px;
+ }
}
}
diff --git a/frontend/src/components/Notifications.js b/frontend/src/components/Notifications.js
new file mode 100644
index 0000000..0b47b43
--- /dev/null
+++ b/frontend/src/components/Notifications.js
@@ -0,0 +1,131 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import React, {Component} from "react";
+import dispatcher from "../dispatcher";
+import {randomClassName} from "../utils";
+import "./Notifications.scss";
+
+const _ = require("lodash");
+const classNames = require("classnames");
+
+class Notifications extends Component {
+
+ state = {
+ notifications: [],
+ closedNotifications: [],
+ };
+
+ componentDidMount() {
+ dispatcher.register("notifications", this.handleNotifications);
+ }
+
+ componentWillUnmount() {
+ dispatcher.unregister(this.handleNotifications);
+ }
+
+ handleNotifications = (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);
+ case "pcap.completed":
+ n.title = "new pcap analyzed";
+ n.description = `${n.message["processed_packets"]} packets processed`;
+ n.variant = "blue";
+ return this.pushNotification(n);
+ default:
+ return null;
+ }
+ };
+
+ pushNotification = (notification) => {
+ const notifications = this.state.notifications;
+ notification.id = randomClassName();
+ notifications.push(notification);
+ this.setState({notifications});
+ setTimeout(() => {
+ const notifications = this.state.notifications;
+ notification.open = true;
+ this.setState({notifications});
+ }, 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 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});
+ };
+ };
+
+ render() {
+ return (
+ <div className="notifications">
+ <div className="notifications-list">
+ {
+ 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 <div key={n.id} className={classNames(notificationClassnames)} onClick={n.onClick}>
+ <h3 className="notification-title">{n.title}</h3>
+ <pre className="notification-description">{n.description}</pre>
+ </div>;
+ })
+ }
+ </div>
+ </div>
+ );
+ }
+}
+
+export default Notifications;
diff --git a/frontend/src/components/Notifications.scss b/frontend/src/components/Notifications.scss
new file mode 100644
index 0000000..5852c7d
--- /dev/null
+++ b/frontend/src/components/Notifications.scss
@@ -0,0 +1,49 @@
+@import "../colors.scss";
+
+.notifications {
+ position: absolute;
+ z-index: 50;
+ bottom: 50px;
+ left: 30px;
+
+ .notification {
+ width: 250px;
+ margin: 10px 0;
+ padding: 10px;
+ cursor: pointer;
+ transition: all 1s ease;
+ transform: translateX(-300px);
+ color: $color-green-light;
+ border-left: 5px solid $color-green-dark;
+ background-color: $color-green;
+
+ .notification-title {
+ font-size: 0.9em;
+ margin: 0;
+ }
+
+ .notification-description {
+ font-size: 0.8em;
+ overflow: hidden;
+ margin: 10px 0;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ color: $color-primary-4;
+ }
+
+ &.notification-open {
+ transform: translateX(0);
+ }
+
+ &.notification-closed {
+ 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
new file mode 100644
index 0000000..9ecbd80
--- /dev/null
+++ b/frontend/src/components/Timeline.js
@@ -0,0 +1,295 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import {TimeRange, TimeSeries} from "pondjs";
+import React, {Component} from "react";
+import {withRouter} from "react-router-dom";
+import {
+ ChartContainer,
+ ChartRow,
+ Charts,
+ LineChart,
+ MultiBrush,
+ Resizable,
+ styler,
+ YAxis
+} from "react-timeseries-charts";
+import backend from "../backend";
+import dispatcher from "../dispatcher";
+import log from "../log";
+import ChoiceField from "./fields/ChoiceField";
+import "./Timeline.scss";
+
+const minutes = 60 * 1000;
+const classNames = require("classnames");
+
+const leftSelectionPaddingMultiplier = 24;
+const rightSelectionPaddingMultiplier = 8;
+
+class Timeline extends Component {
+
+ state = {
+ metric: "connections_per_service"
+ };
+
+ constructor() {
+ super();
+
+ this.disableTimeSeriesChanges = false;
+ this.selectionTimeout = null;
+ }
+
+ componentDidMount() {
+ const urlParams = new URLSearchParams(this.props.location.search);
+ this.setState({
+ servicePortFilter: urlParams.get("service_port") || null,
+ matchedRulesFilter: urlParams.getAll("matched_rules") || null
+ });
+
+ this.loadStatistics(this.state.metric).then(() => log.debug("Statistics loaded after mount"));
+ dispatcher.register("connections_filters", this.handleConnectionsFiltersCallback);
+ dispatcher.register("connection_updates", this.handleConnectionUpdates);
+ dispatcher.register("notifications", this.handleNotifications);
+ dispatcher.register("pulse_timeline", this.handlePulseTimeline);
+ }
+
+ componentWillUnmount() {
+ dispatcher.unregister(this.handleConnectionsFiltersCallback);
+ dispatcher.unregister(this.handleConnectionUpdates);
+ dispatcher.unregister(this.handleNotifications);
+ dispatcher.unregister(this.handlePulseTimeline);
+ }
+
+ loadStatistics = async (metric) => {
+ const urlParams = new URLSearchParams();
+ urlParams.set("metric", metric);
+
+ let columns = [];
+ if (metric === "matched_rules") {
+ let rules = await this.loadRules();
+ if (this.state.matchedRulesFilter.length > 0) {
+ this.state.matchedRulesFilter.forEach((id) => {
+ urlParams.append("rules_ids", id);
+ });
+ columns = this.state.matchedRulesFilter;
+ } else {
+ columns = rules.map((r) => r.id);
+ }
+ } else {
+ let services = await this.loadServices();
+ const filteredPort = this.state.servicePortFilter;
+ if (filteredPort && services[filteredPort]) {
+ const service = services[filteredPort];
+ services = {};
+ services[filteredPort] = service;
+ }
+
+ columns = Object.keys(services);
+ columns.forEach((port) => urlParams.append("ports", port));
+ }
+
+ const metrics = (await backend.get("/api/statistics?" + urlParams)).json;
+ if (metrics.length === 0) {
+ return;
+ }
+
+ const zeroFilledMetrics = [];
+ const toTime = (m) => new Date(m["range_start"]).getTime();
+ let i = 0;
+ for (let interval = toTime(metrics[0]) - minutes; interval <= toTime(metrics[metrics.length - 1]) + minutes; interval += minutes) {
+ if (i < metrics.length && interval === toTime(metrics[i])) {
+ const m = metrics[i++];
+ m["range_start"] = new Date(m["range_start"]);
+ zeroFilledMetrics.push(m);
+ } else {
+ const m = {};
+ m["range_start"] = new Date(interval);
+ m[metric] = {};
+ columns.forEach((c) => m[metric][c] = 0);
+ zeroFilledMetrics.push(m);
+ }
+ }
+
+ const series = new TimeSeries({
+ name: "statistics",
+ columns: ["time"].concat(columns),
+ points: zeroFilledMetrics.map((m) => [m["range_start"]].concat(columns.map((c) =>
+ ((metric in m) && (m[metric] != null)) ? (m[metric][c] || 0) : 0
+ )))
+ });
+
+ const start = series.range().begin();
+ const end = series.range().end();
+
+ this.setState({
+ metric,
+ series,
+ timeRange: new TimeRange(start, end),
+ columns,
+ start,
+ end
+ });
+ };
+
+ loadServices = async () => {
+ const services = (await backend.get("/api/services")).json;
+ this.setState({services});
+ return services;
+ };
+
+ loadRules = async () => {
+ const rules = (await backend.get("/api/rules")).json;
+ this.setState({rules});
+ return rules;
+ };
+
+ createStyler = () => {
+ if (this.state.metric === "matched_rules") {
+ return styler(this.state.rules.map((rule) => {
+ return {key: rule.id, color: rule.color, width: 2};
+ }));
+ } else {
+ return styler(Object.keys(this.state.services).map((port) => {
+ return {key: port, color: this.state.services[port].color, width: 2};
+ }));
+ }
+ };
+
+ handleTimeRangeChange = (timeRange) => {
+ if (!this.disableTimeSeriesChanges) {
+ this.setState({timeRange});
+ }
+ };
+
+ handleSelectionChange = (timeRange) => {
+ this.disableTimeSeriesChanges = true;
+
+ this.setState({selection: timeRange});
+ if (this.selectionTimeout) {
+ clearTimeout(this.selectionTimeout);
+ }
+ this.selectionTimeout = setTimeout(() => {
+ dispatcher.dispatch("timeline_updates", {
+ from: timeRange.begin(),
+ to: timeRange.end()
+ });
+ this.selectionTimeout = null;
+ this.disableTimeSeriesChanges = false;
+ }, 1000);
+ };
+
+ handleConnectionsFiltersCallback = (payload) => {
+ if ("service_port" in payload && this.state.servicePortFilter !== payload["service_port"]) {
+ this.setState({servicePortFilter: payload["service_port"]});
+ this.loadStatistics(this.state.metric).then(() => log.debug("Statistics reloaded after service port changed"));
+ }
+ if ("matched_rules" in payload && this.state.matchedRulesFilter !== payload["matched_rules"]) {
+ this.setState({matchedRulesFilter: payload["matched_rules"]});
+ this.loadStatistics(this.state.metric).then(() => log.debug("Statistics reloaded after matched rules changed"));
+ }
+ };
+
+ handleConnectionUpdates = (payload) => {
+ this.setState({
+ selection: new TimeRange(payload.from, payload.to),
+ });
+ this.adjustSelection();
+ };
+
+ handleNotifications = (payload) => {
+ if (payload.event === "services.edit" && this.state.metric !== "matched_rules") {
+ this.loadStatistics(this.state.metric).then(() => log.debug("Statistics reloaded after services updates"));
+ } else if (payload.event.startsWith("rules") && this.state.metric === "matched_rules") {
+ this.loadStatistics(this.state.metric).then(() => log.debug("Statistics reloaded after rules updates"));
+ } else if (payload.event === "pcap.completed") {
+ this.loadStatistics(this.state.metric).then(() => log.debug("Statistics reloaded after pcap processed"));
+ }
+ };
+
+ handlePulseTimeline = (payload) => {
+ this.setState({pulseTimeline: true});
+ setTimeout(() => this.setState({pulseTimeline: false}), payload.duration);
+ };
+
+ adjustSelection = () => {
+ const seriesRange = this.state.series.range();
+ const selection = this.state.selection;
+ const delta = selection.end() - selection.begin();
+ const start = Math.max(selection.begin().getTime() - delta * leftSelectionPaddingMultiplier, seriesRange.begin().getTime());
+ const end = Math.min(selection.end().getTime() + delta * rightSelectionPaddingMultiplier, seriesRange.end().getTime());
+ this.setState({timeRange: new TimeRange(start, end)});
+ };
+
+ aggregateSeries = (func) => {
+ const values = this.state.series.columns().map((c) => this.state.series[func](c));
+ return Math[func](...values);
+ };
+
+ render() {
+ if (!this.state.series) {
+ return null;
+ }
+
+ return (
+ <footer className="footer">
+ <div className={classNames("time-line", {"pulse-timeline": this.state.pulseTimeline})}>
+ <Resizable>
+ <ChartContainer timeRange={this.state.timeRange} enableDragZoom={false}
+ paddingTop={5} minDuration={60000}
+ maxTime={this.state.end}
+ minTime={this.state.start}
+ paddingLeft={0} paddingRight={0} paddingBottom={0}
+ enablePanZoom={true} utc={false}
+ onTimeRangeChanged={this.handleTimeRangeChange}>
+
+ <ChartRow height="125">
+ <YAxis id="axis1" hideAxisLine
+ min={this.aggregateSeries("min")}
+ max={this.aggregateSeries("max")} width="35" type="linear" transition={300}/>
+ <Charts>
+ <LineChart axis="axis1" series={this.state.series}
+ columns={this.state.columns}
+ style={this.createStyler()} interpolation="curveBasis"/>
+
+ <MultiBrush
+ timeRanges={[this.state.selection]}
+ allowSelectionClear={false}
+ allowFreeDrawing={false}
+ onTimeRangeChanged={this.handleSelectionChange}
+ />
+ </Charts>
+ </ChartRow>
+ </ChartContainer>
+ </Resizable>
+
+ <div className="metric-selection">
+ <ChoiceField inline small
+ keys={["connections_per_service", "client_bytes_per_service",
+ "server_bytes_per_service", "duration_per_service", "matched_rules"]}
+ values={["connections_per_service", "client_bytes_per_service",
+ "server_bytes_per_service", "duration_per_service", "matched_rules"]}
+ onChange={(metric) => this.loadStatistics(metric)
+ .then(() => log.debug("Statistics loaded after metric changes"))}
+ value={this.state.metric}/>
+ </div>
+ </div>
+ </footer>
+ );
+ }
+}
+
+export default withRouter(Timeline);
diff --git a/frontend/src/components/Timeline.scss b/frontend/src/components/Timeline.scss
new file mode 100644
index 0000000..262da1e
--- /dev/null
+++ b/frontend/src/components/Timeline.scss
@@ -0,0 +1,27 @@
+@import "../colors";
+
+.footer {
+ padding: 15px;
+
+ .time-line {
+ position: relative;
+ background-color: $color-primary-0;
+
+ .metric-selection {
+ font-size: 0.8em;
+ position: absolute;
+ top: 5px;
+ right: 10px;
+ width: 180px;
+ }
+
+ &.pulse-timeline {
+ animation: pulse 2s infinite;
+ }
+ }
+
+ svg text {
+ font-family: "Fira Code", monospace !important;
+ fill: $color-primary-4 !important;
+ }
+}
diff --git a/frontend/src/components/dialogs/Filters.js b/frontend/src/components/dialogs/Filters.js
new file mode 100644
index 0000000..a2407df
--- /dev/null
+++ b/frontend/src/components/dialogs/Filters.js
@@ -0,0 +1,85 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import React, {Component} from "react";
+import {Modal} from "react-bootstrap";
+import {cleanNumber, validateIpAddress, validateMin, validatePort} from "../../utils";
+import ButtonField from "../fields/ButtonField";
+import StringConnectionsFilter from "../filters/StringConnectionsFilter";
+import "./Filters.scss";
+
+class Filters extends Component {
+
+ render() {
+ return (
+ <Modal
+ {...this.props}
+ show={true}
+ size="lg"
+ aria-labelledby="filters-dialog"
+ centered
+ >
+ <Modal.Header>
+ <Modal.Title id="filters-dialog">
+ ~/advanced_filters
+ </Modal.Title>
+ </Modal.Header>
+ <Modal.Body>
+ <div className="advanced-filters d-flex">
+ <div className="flex-fill">
+ <StringConnectionsFilter filterName="client_address"
+ defaultFilterValue="all_addresses"
+ validateFunc={validateIpAddress}
+ key="client_address_filter"/>
+ <StringConnectionsFilter filterName="min_duration"
+ defaultFilterValue="0"
+ replaceFunc={cleanNumber}
+ validateFunc={validateMin(0)}
+ key="min_duration_filter"/>
+ <StringConnectionsFilter filterName="min_bytes"
+ defaultFilterValue="0"
+ replaceFunc={cleanNumber}
+ validateFunc={validateMin(0)}
+ key="min_bytes_filter"/>
+ </div>
+
+ <div className="flex-fill">
+ <StringConnectionsFilter filterName="client_port"
+ defaultFilterValue="all_ports"
+ replaceFunc={cleanNumber}
+ validateFunc={validatePort}
+ key="client_port_filter"/>
+ <StringConnectionsFilter filterName="max_duration"
+ defaultFilterValue="∞"
+ replaceFunc={cleanNumber}
+ key="max_duration_filter"/>
+ <StringConnectionsFilter filterName="max_bytes"
+ defaultFilterValue="∞"
+ replaceFunc={cleanNumber}
+ key="max_bytes_filter"/>
+ </div>
+ </div>
+ </Modal.Body>
+ <Modal.Footer className="dialog-footer">
+ <ButtonField variant="red" bordered onClick={this.props.onHide} name="close"/>
+ </Modal.Footer>
+ </Modal>
+ );
+ }
+}
+
+export default Filters;
diff --git a/frontend/src/components/dialogs/Filters.scss b/frontend/src/components/dialogs/Filters.scss
new file mode 100644
index 0000000..7d09380
--- /dev/null
+++ b/frontend/src/components/dialogs/Filters.scss
@@ -0,0 +1,5 @@
+.advanced-filters {
+ .filter {
+ margin: 10px;
+ }
+}
diff --git a/frontend/src/components/fields/ButtonField.js b/frontend/src/components/fields/ButtonField.js
index cc32b0f..15ef179 100644
--- a/frontend/src/components/fields/ButtonField.js
+++ b/frontend/src/components/fields/ButtonField.js
@@ -1,8 +1,25 @@
-import React, {Component} from 'react';
-import './ButtonField.scss';
-import './common.scss';
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
-const classNames = require('classnames');
+import React, {Component} from "react";
+import "./ButtonField.scss";
+import "./common.scss";
+
+const classNames = require("classnames");
class ButtonField extends Component {
@@ -38,9 +55,10 @@ class ButtonField extends Component {
}
return (
- <div className={classNames( "field", "button-field", {"field-small": this.props.small})}>
- <button type="button" className={classNames(classNames(buttonClassnames))}
- onClick={handler} style={buttonStyle}>{this.props.name}</button>
+ <div className={classNames("field", "button-field", {"field-small": this.props.small},
+ {"field-active": this.props.active})}>
+ <button type="button" className={classNames(buttonClassnames)}
+ onClick={handler} style={buttonStyle} disabled={this.props.disabled}>{this.props.name}</button>
</div>
);
}
diff --git a/frontend/src/components/fields/ButtonField.scss b/frontend/src/components/fields/ButtonField.scss
index 9e46b9f..99afe08 100644
--- a/frontend/src/components/fields/ButtonField.scss
+++ b/frontend/src/components/fields/ButtonField.scss
@@ -15,6 +15,13 @@
}
}
+ &.field-active {
+ button {
+ color: $color-primary-1;
+ background-color: $color-primary-4;
+ }
+ }
+
.button-variant-red {
color: $color-red-light;
background-color: $color-red;
diff --git a/frontend/src/components/fields/CheckField.js b/frontend/src/components/fields/CheckField.js
index 33f4f83..bfa1c9d 100644
--- a/frontend/src/components/fields/CheckField.js
+++ b/frontend/src/components/fields/CheckField.js
@@ -1,9 +1,26 @@
-import React, {Component} from 'react';
-import './CheckField.scss';
-import './common.scss';
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import React, {Component} from "react";
import {randomClassName} from "../../utils";
+import "./CheckField.scss";
+import "./common.scss";
-const classNames = require('classnames');
+const classNames = require("classnames");
class CheckField extends Component {
@@ -18,15 +35,15 @@ class CheckField extends Component {
const small = this.props.small || false;
const name = this.props.name || null;
const handler = () => {
- if (this.props.onChange) {
+ if (!this.props.readonly && this.props.onChange) {
this.props.onChange(!checked);
}
};
return (
- <div className={classNames( "field", "check-field", {"field-checked" : checked}, {"field-small": small})}>
+ <div className={classNames("field", "check-field", {"field-checked": checked}, {"field-small": small})}>
<div className="field-input">
- <input type="checkbox" id={this.id} checked={checked} onChange={handler} />
+ <input type="checkbox" id={this.id} checked={checked} onChange={handler}/>
<label htmlFor={this.id}>{(checked ? "✓ " : "✗ ") + (name != null ? name : "")}</label>
</div>
</div>
diff --git a/frontend/src/components/fields/ChoiceField.js b/frontend/src/components/fields/ChoiceField.js
index 73e950d..7e97d89 100644
--- a/frontend/src/components/fields/ChoiceField.js
+++ b/frontend/src/components/fields/ChoiceField.js
@@ -1,9 +1,26 @@
-import React, {Component} from 'react';
-import './ChoiceField.scss';
-import './common.scss';
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import React, {Component} from "react";
import {randomClassName} from "../../utils";
+import "./ChoiceField.scss";
+import "./common.scss";
-const classNames = require('classnames');
+const classNames = require("classnames");
class ChoiceField extends Component {
@@ -50,7 +67,7 @@ class ChoiceField extends Component {
}
return (
- <div className={classNames( "field", "choice-field", {"field-inline" : inline},
+ <div className={classNames("field", "choice-field", {"field-inline": inline},
{"field-small": this.props.small})}>
{!inline && name && <label className="field-name">{name}:</label>}
<div className={classNames("field-select", {"select-expanded": this.state.expanded})}
diff --git a/frontend/src/components/fields/ChoiceField.scss b/frontend/src/components/fields/ChoiceField.scss
index 0b5e510..85986af 100644
--- a/frontend/src/components/fields/ChoiceField.scss
+++ b/frontend/src/components/fields/ChoiceField.scss
@@ -19,7 +19,7 @@
border-radius: 5px;
background-color: $color-primary-2;
- &:after {
+ &::after {
position: absolute;
right: 10px;
content: "⋎";
@@ -27,8 +27,8 @@
}
.field-options {
- position: absolute;
- z-index: 20;
+ position: static;
+ z-index: 100;
top: 35px;
display: none;
width: 100%;
@@ -58,7 +58,7 @@
display: block;
}
- .field-value:after {
+ .field-value::after {
content: "⋏";
}
}
diff --git a/frontend/src/components/fields/InputField.js b/frontend/src/components/fields/InputField.js
index 84c981b..e2ea020 100644
--- a/frontend/src/components/fields/InputField.js
+++ b/frontend/src/components/fields/InputField.js
@@ -1,9 +1,26 @@
-import React, {Component} from 'react';
-import './InputField.scss';
-import './common.scss';
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import React, {Component} from "react";
import {randomClassName} from "../../utils";
+import "./common.scss";
+import "./InputField.scss";
-const classNames = require('classnames');
+const classNames = require("classnames");
class InputField extends Component {
@@ -42,23 +59,23 @@ class InputField extends Component {
}
return (
- <div className={classNames("field", "input-field", {"field-active" : active},
+ <div className={classNames("field", "input-field", {"field-active": active},
{"field-invalid": invalid}, {"field-small": small}, {"field-inline": inline})}>
<div className="field-wrapper">
- { name &&
+ {name &&
<div className="field-name">
<label>{name}:</label>
</div>
}
<div className="field-input">
<div className="field-value">
- { type === "file" && <label for={this.id} className={"file-label"}>
- {value.name || this.props.placeholder}</label> }
+ {type === "file" && <label for={this.id} className={"file-label"}>
+ {value.name || this.props.placeholder}</label>}
<input type={type} placeholder={this.props.placeholder} id={this.id}
aria-describedby={this.id} onChange={handler} {...inputProps}
- readOnly={this.props.readonly} />
+ readOnly={this.props.readonly}/>
</div>
- { type !== "file" && value !== "" &&
+ {type !== "file" && value !== "" && !this.props.readonly &&
<div className="field-clear">
<span onClick={() => handler(null)}>del</span>
</div>
diff --git a/frontend/src/components/fields/InputField.scss b/frontend/src/components/fields/InputField.scss
index 7cc34d9..eafb2ab 100644
--- a/frontend/src/components/fields/InputField.scss
+++ b/frontend/src/components/fields/InputField.scss
@@ -28,7 +28,7 @@
display: none;
}
- .file-label:after {
+ .file-label::after {
position: absolute;
top: 0;
right: 0;
@@ -47,13 +47,14 @@
background-color: $color-primary-4 !important;
}
- .field-value input, .field-value .file-label {
- color: $color-primary-3 !important;
- background-color: $color-primary-4 !important;
+ .file-label::after {
+ background-color: $color-secondary-4 !important;
}
- .file-label:after {
- background-color: $color-secondary-4 !important;
+ .field-value input,
+ .field-value .file-label {
+ color: $color-primary-3 !important;
+ background-color: $color-primary-4 !important;
}
}
@@ -63,12 +64,13 @@
background-color: $color-secondary-2 !important;
}
- .field-value input, .field-value .file-label {
+ .field-value input,
+ .field-value .file-label {
color: $color-primary-4 !important;
background-color: $color-secondary-2 !important;
}
- .file-label:after {
+ .file-label::after {
background-color: $color-secondary-1 !important;
}
}
@@ -90,7 +92,8 @@
.field-input {
width: 100%;
- input, .file-label {
+ input,
+ .file-label {
padding-left: 3px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
diff --git a/frontend/src/components/fields/TagField.js b/frontend/src/components/fields/TagField.js
new file mode 100644
index 0000000..9a36da4
--- /dev/null
+++ b/frontend/src/components/fields/TagField.js
@@ -0,0 +1,75 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import React, {Component} from "react";
+import ReactTags from "react-tag-autocomplete";
+import {randomClassName} from "../../utils";
+import "./common.scss";
+import "./TagField.scss";
+
+const classNames = require("classnames");
+const _ = require("lodash");
+
+class TagField extends Component {
+
+ state = {};
+
+ constructor(props) {
+ super(props);
+
+ this.id = `field-${this.props.name || "noname"}-${randomClassName()}`;
+ }
+
+ onAddition = (tag) => {
+ if (typeof this.props.onChange === "function") {
+ this.props.onChange([].concat(this.props.tags, tag), true, tag); // true == addition
+ }
+ };
+
+ onDelete = (i) => {
+ if (typeof this.props.onChange === "function") {
+ const tags = _.clone(this.props.tags);
+ const tag = tags[i];
+ tags.splice(i, 1);
+ this.props.onChange(tags, true, tag); // false == delete
+ }
+ };
+
+
+ render() {
+ const small = this.props.small || false;
+ const name = this.props.name || null;
+
+ return (
+ <div className={classNames("field", "tag-field", {"field-small": small},
+ {"field-inline": this.props.inline})}>
+ {name &&
+ <div className="field-name">
+ <label>{name}:</label>
+ </div>
+ }
+ <div className="field-input">
+ <ReactTags {...this.props} tags={this.props.tags || []} autoresize={false}
+ onDelete={this.onDelete} onAddition={this.onAddition}
+ placeholderText={this.props.placeholder || ""}/>
+ </div>
+ </div>
+ );
+ }
+}
+
+export default TagField;
diff --git a/frontend/src/components/fields/TagField.scss b/frontend/src/components/fields/TagField.scss
new file mode 100644
index 0000000..723e71f
--- /dev/null
+++ b/frontend/src/components/fields/TagField.scss
@@ -0,0 +1,157 @@
+@import "../../colors.scss";
+
+.tag-field {
+ font-size: 0.9em;
+ margin: 5px 0;
+
+ .field-name {
+ label {
+ margin: 0;
+ }
+ }
+
+ .react-tags {
+ position: relative;
+ display: flex;
+ border-radius: 4px;
+ background-color: $color-primary-2;
+
+ &:focus-within,
+ &:focus-within .react-tags__search-input {
+ background-color: $color-primary-1;
+ }
+ }
+
+ &.field-small {
+ font-size: 0.8em;
+ }
+
+ &.field-inline {
+ display: flex;
+
+ .field-name {
+ padding: 6px 0 6px 7px;
+ border-top-left-radius: 5px;
+ border-bottom-left-radius: 5px;
+ background-color: $color-primary-2;
+ }
+
+ .field-input {
+ flex: 1;
+
+ .react-tags {
+ padding-left: 3px;
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+ }
+
+ &:focus-within .field-name {
+ background-color: $color-primary-1;
+ }
+ }
+
+ .react-tags__selected {
+ display: inline-block;
+ flex: 0 1;
+ margin: 6px 0;
+ white-space: nowrap;
+ }
+
+ .react-tags__selected-tag {
+ font-size: 0.75em;
+ margin: 0 3px;
+ padding: 2px 4px;
+ color: $color-primary-3;
+ border-radius: 2px;
+ background: $color-primary-4;
+ }
+
+ .react-tags__selected-tag::after {
+ margin-left: 8px;
+ content: "\2715";
+ color: $color-primary-3;
+ }
+
+ .react-tags__selected-tag:hover,
+ .react-tags__selected-tag:focus {
+ border-color: #b1b1b1;
+ background-color: $color-primary-0;
+
+ &::after {
+ color: $color-primary-4;
+ }
+ }
+
+ .react-tags__search {
+ flex: 1 0;
+ }
+
+ @media screen and (min-width: 30em) {
+ .react-tags__search {
+ position: relative;
+ }
+ }
+
+ .react-tags__search-input {
+ color: $color-primary-4;
+ background-color: $color-primary-2;
+ }
+
+ .react-tags__search-input::-ms-clear {
+ display: none;
+ }
+
+ .react-tags__suggestions {
+ position: absolute;
+ z-index: 50;
+ top: 100%;
+ left: 0;
+ width: 100%;
+ }
+
+ @media screen and (min-width: 30em) {
+ .react-tags__suggestions {
+ width: 240px;
+ }
+ }
+
+ .react-tags__suggestions ul {
+ font-size: 12px;
+ margin: 4px -1px;
+ padding: 0;
+ list-style: none;
+ border-radius: 3px;
+ background: $color-primary-2;
+ }
+
+ .react-tags__suggestions li {
+ padding: 5px 10px;
+ }
+
+ .react-tags__suggestions li mark {
+ font-weight: 600;
+ padding: 0;
+ color: $color-primary-4;
+ background: none;
+ }
+
+ .react-tags__suggestions li:hover {
+ cursor: pointer;
+ border-radius: 3px;
+ background: $color-primary-1;
+
+ mark {
+ color: $color-primary-4;
+ }
+ }
+
+ .react-tags__suggestions li.is-active {
+ background: $color-primary-3;
+ }
+
+ .react-tags__suggestions li.is-disabled {
+ cursor: auto;
+ opacity: 0.5;
+ }
+}
diff --git a/frontend/src/components/fields/TextField.js b/frontend/src/components/fields/TextField.js
index de68c21..4dd77bd 100644
--- a/frontend/src/components/fields/TextField.js
+++ b/frontend/src/components/fields/TextField.js
@@ -1,9 +1,26 @@
-import React, {Component} from 'react';
-import './TextField.scss';
-import './common.scss';
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import React, {Component} from "react";
import {randomClassName} from "../../utils";
+import "./common.scss";
+import "./TextField.scss";
-const classNames = require('classnames');
+const classNames = require("classnames");
class TextField extends Component {
@@ -33,7 +50,7 @@ class TextField extends Component {
{"field-invalid": this.props.invalid}, {"field-small": this.props.small})}>
{name && <label htmlFor={this.id}>{name}:</label>}
<textarea id={this.id} placeholder={this.props.defaultValue} onChange={handler} rows={rows}
- readOnly={this.props.readonly} value={this.props.value} ref={this.props.textRef} />
+ readOnly={this.props.readonly} value={this.props.value} ref={this.props.textRef}/>
{error && <div className="field-error">error: {error}</div>}
</div>
);
diff --git a/frontend/src/components/fields/TextField.scss b/frontend/src/components/fields/TextField.scss
index c2d6ef5..5fde9e6 100644
--- a/frontend/src/components/fields/TextField.scss
+++ b/frontend/src/components/fields/TextField.scss
@@ -51,4 +51,8 @@
padding: 5px 10px;
color: $color-secondary-0;
}
+
+ &:hover::-webkit-scrollbar-thumb {
+ background: $color-secondary-2 !important;
+ }
}
diff --git a/frontend/src/components/fields/common.scss b/frontend/src/components/fields/common.scss
index f37369e..e5dc65c 100644
--- a/frontend/src/components/fields/common.scss
+++ b/frontend/src/components/fields/common.scss
@@ -1,7 +1,9 @@
@import "../../colors.scss";
.field {
- input, textarea {
+ input,
+ textarea {
+ font-family: "Fira Code", monospace;
width: 100%;
padding: 7px 10px;
color: $color-primary-4;
diff --git a/frontend/src/components/fields/extensions/ColorField.js b/frontend/src/components/fields/extensions/ColorField.js
index 96ebc49..fd30988 100644
--- a/frontend/src/components/fields/extensions/ColorField.js
+++ b/frontend/src/components/fields/extensions/ColorField.js
@@ -1,19 +1,35 @@
-import React, {Component} from 'react';
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import React, {Component} from "react";
import {OverlayTrigger, Popover} from "react-bootstrap";
-import './ColorField.scss';
-import InputField from "../InputField";
import validation from "../../../validation";
+import InputField from "../InputField";
+import "./ColorField.scss";
class ColorField extends Component {
constructor(props) {
super(props);
- this.state = {
- };
+ this.state = {};
- this.colors = ["#E53935", "#D81B60", "#8E24AA", "#5E35B1", "#3949AB", "#1E88E5", "#039BE5", "#00ACC1",
- "#00897B", "#43A047", "#7CB342", "#9E9D24", "#F9A825", "#FB8C00", "#F4511E", "#6D4C41"];
+ this.colors = ["#e53935", "#d81b60", "#8e24aa", "#5e35b1", "#3949ab", "#1e88e5", "#039be5", "#00acc1",
+ "#00897b", "#43a047", "#7cb342", "#9e9d24", "#f9a825", "#fb8c00", "#f4511e", "#6d4c41"];
}
componentDidUpdate(prevProps, prevState, snapshot) {
@@ -38,7 +54,7 @@ class ColorField extends Component {
this.props.onChange(color);
}
document.body.click(); // magic to close popup
- }} />);
+ }}/>);
const popover = (
<Popover id="popover-basic">
@@ -65,7 +81,7 @@ class ColorField extends Component {
<div className="field color-field">
<div className="color-input">
<InputField {...this.props} onChange={this.onChange} invalid={this.state.invalid} name="color"
- error={null} />
+ error={null}/>
<div className="color-picker">
<OverlayTrigger trigger="click" placement="top" overlay={popover} rootClose>
<button type="button" className="picker-button" style={buttonStyles}>pick</button>
diff --git a/frontend/src/components/fields/extensions/NumericField.js b/frontend/src/components/fields/extensions/NumericField.js
index 19a9e46..a6cba26 100644
--- a/frontend/src/components/fields/extensions/NumericField.js
+++ b/frontend/src/components/fields/extensions/NumericField.js
@@ -1,4 +1,21 @@
-import React, {Component} from 'react';
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import React, {Component} from "react";
import InputField from "../InputField";
class NumericField extends Component {
@@ -18,7 +35,7 @@ class NumericField extends Component {
}
onChange = (value) => {
- value = value.toString().replace(/[^\d]/gi, '');
+ value = value.toString().replace(/[^\d]/gi, "");
let intValue = 0;
if (value !== "") {
intValue = parseInt(value, 10);
@@ -36,7 +53,7 @@ class NumericField extends Component {
render() {
return (
<InputField {...this.props} onChange={this.onChange} defaultValue={this.props.defaultValue || "0"}
- invalid={this.state.invalid} />
+ invalid={this.state.invalid}/>
);
}
diff --git a/frontend/src/components/filters/AdvancedFilters.js b/frontend/src/components/filters/AdvancedFilters.js
new file mode 100644
index 0000000..8598185
--- /dev/null
+++ b/frontend/src/components/filters/AdvancedFilters.js
@@ -0,0 +1,54 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import React, {Component} from "react";
+import {withRouter} from "react-router-dom";
+import dispatcher from "../../dispatcher";
+import {updateParams} from "../../utils";
+import ButtonField from "../fields/ButtonField";
+
+class AdvancedFilters extends Component {
+
+ state = {};
+
+ componentDidMount() {
+ this.urlParams = new URLSearchParams(this.props.location.search);
+
+ this.connectionsFiltersCallback = (payload) => {
+ this.urlParams = updateParams(this.urlParams, payload);
+ const active = ["client_address", "client_port", "min_duration", "max_duration", "min_bytes", "max_bytes"]
+ .some((f) => this.urlParams.has(f));
+ if (this.state.active !== active) {
+ this.setState({active});
+ }
+ };
+ dispatcher.register("connections_filters", this.connectionsFiltersCallback);
+ }
+
+ componentWillUnmount() {
+ dispatcher.unregister(this.connectionsFiltersCallback);
+ }
+
+ render() {
+ return (
+ <ButtonField onClick={this.props.onClick} name="advanced_filters" small active={this.state.active}/>
+ );
+ }
+
+}
+
+export default withRouter(AdvancedFilters);
diff --git a/frontend/src/components/filters/BooleanConnectionsFilter.js b/frontend/src/components/filters/BooleanConnectionsFilter.js
index 4c5a78a..0355167 100644
--- a/frontend/src/components/filters/BooleanConnectionsFilter.js
+++ b/frontend/src/components/filters/BooleanConnectionsFilter.js
@@ -1,64 +1,65 @@
-import React, {Component} from 'react';
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import React, {Component} from "react";
import {withRouter} from "react-router-dom";
-import {Redirect} from "react-router";
+import dispatcher from "../../dispatcher";
import CheckField from "../fields/CheckField";
class BooleanConnectionsFilter extends Component {
- constructor(props) {
- super(props);
- this.state = {
- filterActive: "false"
- };
-
- this.filterChanged = this.filterChanged.bind(this);
- this.needRedirect = false;
- }
+ state = {
+ filterActive: "false"
+ };
componentDidMount() {
let params = new URLSearchParams(this.props.location.search);
this.setState({filterActive: this.toBoolean(params.get(this.props.filterName)).toString()});
+
+ this.connectionsFiltersCallback = (payload) => {
+ const name = this.props.filterName;
+ if (name in payload && this.state.filterActive !== payload[name]) {
+ this.setState({filterActive: payload[name]});
+ }
+ };
+ dispatcher.register("connections_filters", this.connectionsFiltersCallback);
}
- componentDidUpdate(prevProps, prevState, snapshot) {
- let urlParams = new URLSearchParams(this.props.location.search);
- let externalActive = this.toBoolean(urlParams.get(this.props.filterName));
- let filterActive = this.toBoolean(this.state.filterActive);
- // if the filterActive state is changed by another component (and not by filterChanged func) and
- // the query string is not equals at the filterActive state, update the state of the component
- if (this.toBoolean(prevState.filterActive) === filterActive && filterActive !== externalActive) {
- this.setState({filterActive: externalActive.toString()});
- }
+ componentWillUnmount() {
+ dispatcher.unregister(this.connectionsFiltersCallback);
}
- toBoolean(value) {
+ toBoolean = (value) => {
return value !== null && value.toLowerCase() === "true";
- }
+ };
- filterChanged() {
- this.needRedirect = true;
- this.setState({filterActive: (!this.toBoolean(this.state.filterActive)).toString()});
- }
+ filterChanged = () => {
+ const newValue = (!this.toBoolean(this.state.filterActive)).toString();
+ const urlParams = {};
+ urlParams[this.props.filterName] = newValue === "true" ? "true" : null;
+ dispatcher.dispatch("connections_filters", urlParams);
+ this.setState({filterActive: newValue});
+ };
render() {
- let redirect = null;
- if (this.needRedirect) {
- let urlParams = new URLSearchParams(this.props.location.search);
- if (this.toBoolean(this.state.filterActive)) {
- urlParams.set(this.props.filterName, "true");
- } else {
- urlParams.delete(this.props.filterName);
- }
- redirect = <Redirect push to={`${this.props.location.pathname}?${urlParams}`} />;
-
- this.needRedirect = false;
- }
-
return (
<div className="filter" style={{"width": `${this.props.width}px`}}>
<CheckField checked={this.toBoolean(this.state.filterActive)} name={this.props.filterName}
- onChange={this.filterChanged} />
- {redirect}
+ onChange={this.filterChanged} small/>
</div>
);
}
diff --git a/frontend/src/components/filters/ExitSearchFilter.js b/frontend/src/components/filters/ExitSearchFilter.js
new file mode 100644
index 0000000..0aacfd6
--- /dev/null
+++ b/frontend/src/components/filters/ExitSearchFilter.js
@@ -0,0 +1,57 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import React, {Component} from "react";
+import {withRouter} from "react-router-dom";
+import dispatcher from "../../dispatcher";
+import CheckField from "../fields/CheckField";
+
+class ExitSearchFilter extends Component {
+
+ state = {};
+
+ componentDidMount() {
+ let params = new URLSearchParams(this.props.location.search);
+ this.setState({performedSearch: params.get("performed_search")});
+
+ this.connectionsFiltersCallback = (payload) => {
+ if (this.state.performedSearch !== payload["performed_search"]) {
+ this.setState({performedSearch: payload["performed_search"]});
+ }
+ };
+ dispatcher.register("connections_filters", this.connectionsFiltersCallback);
+ }
+
+ componentWillUnmount() {
+ dispatcher.unregister(this.connectionsFiltersCallback);
+ }
+
+ render() {
+ return (
+ <>
+ {this.state.performedSearch &&
+ <div className="filter" style={{"width": `${this.props.width}px`}}>
+ <CheckField checked={true} name="exit_search" onChange={() =>
+ dispatcher.dispatch("connections_filters", {"performed_search": null})} small/>
+ </div>}
+ </>
+ );
+ }
+
+}
+
+export default withRouter(ExitSearchFilter);
diff --git a/frontend/src/components/filters/FiltersDefinitions.js b/frontend/src/components/filters/FiltersDefinitions.js
deleted file mode 100644
index 02ccb42..0000000
--- a/frontend/src/components/filters/FiltersDefinitions.js
+++ /dev/null
@@ -1,90 +0,0 @@
-import {
- cleanNumber,
- timestampToTime,
- timeToTimestamp,
- validate24HourTime,
- validateIpAddress,
- validateMin,
- validatePort
-} from "../../utils";
-import StringConnectionsFilter from "./StringConnectionsFilter";
-import React from "react";
-import RulesConnectionsFilter from "./RulesConnectionsFilter";
-import BooleanConnectionsFilter from "./BooleanConnectionsFilter";
-
-export const filtersNames = ["service_port", "matched_rules", "client_address", "client_port",
- "min_duration", "max_duration", "min_bytes", "max_bytes", "started_after",
- "started_before", "closed_after", "closed_before", "marked", "hidden"];
-
-export const filtersDefinitions = {
- service_port: <StringConnectionsFilter filterName="service_port"
- defaultFilterValue="all_ports"
- replaceFunc={cleanNumber}
- validateFunc={validatePort}
- key="service_port_filter"
- width={200} />,
- matched_rules: <RulesConnectionsFilter />,
- client_address: <StringConnectionsFilter filterName="client_address"
- defaultFilterValue="all_addresses"
- validateFunc={validateIpAddress}
- key="client_address_filter"
- width={320} />,
- client_port: <StringConnectionsFilter filterName="client_port"
- defaultFilterValue="all_ports"
- replaceFunc={cleanNumber}
- validateFunc={validatePort}
- key="client_port_filter"
- width={200} />,
- min_duration: <StringConnectionsFilter filterName="min_duration"
- defaultFilterValue="0"
- replaceFunc={cleanNumber}
- validateFunc={validateMin(0)}
- key="min_duration_filter"
- width={200} />,
- max_duration: <StringConnectionsFilter filterName="max_duration"
- defaultFilterValue="∞"
- replaceFunc={cleanNumber}
- key="max_duration_filter"
- width={200} />,
- min_bytes: <StringConnectionsFilter filterName="min_bytes"
- defaultFilterValue="0"
- replaceFunc={cleanNumber}
- validateFunc={validateMin(0)}
- key="min_bytes_filter"
- width={200} />,
- max_bytes: <StringConnectionsFilter filterName="max_bytes"
- defaultFilterValue="∞"
- replaceFunc={cleanNumber}
- key="max_bytes_filter"
- width={200} />,
- started_after: <StringConnectionsFilter filterName="started_after"
- defaultFilterValue="00:00:00"
- validateFunc={validate24HourTime}
- encodeFunc={timeToTimestamp}
- decodeFunc={timestampToTime}
- key="started_after_filter"
- width={200} />,
- started_before: <StringConnectionsFilter filterName="started_before"
- defaultFilterValue="00:00:00"
- validateFunc={validate24HourTime}
- encodeFunc={timeToTimestamp}
- decodeFunc={timestampToTime}
- key="started_before_filter"
- width={200} />,
- closed_after: <StringConnectionsFilter filterName="closed_after"
- defaultFilterValue="00:00:00"
- validateFunc={validate24HourTime}
- encodeFunc={timeToTimestamp}
- decodeFunc={timestampToTime}
- key="closed_after_filter"
- width={200} />,
- closed_before: <StringConnectionsFilter filterName="closed_before"
- defaultFilterValue="00:00:00"
- validateFunc={validate24HourTime}
- encodeFunc={timeToTimestamp}
- decodeFunc={timestampToTime}
- key="closed_before_filter"
- width={200} />,
- marked: <BooleanConnectionsFilter filterName={"marked"} />,
- hidden: <BooleanConnectionsFilter filterName={"hidden"} />
-};
diff --git a/frontend/src/components/filters/RulesConnectionsFilter.js b/frontend/src/components/filters/RulesConnectionsFilter.js
index 8366189..210ee36 100644
--- a/frontend/src/components/filters/RulesConnectionsFilter.js
+++ b/frontend/src/components/filters/RulesConnectionsFilter.js
@@ -1,86 +1,79 @@
-import React, {Component} from 'react';
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import React, {Component} from "react";
import {withRouter} from "react-router-dom";
-import {Redirect} from "react-router";
-import './RulesConnectionsFilter.scss';
-import ReactTags from 'react-tag-autocomplete';
import backend from "../../backend";
+import dispatcher from "../../dispatcher";
+import TagField from "../fields/TagField";
-const classNames = require('classnames');
+const classNames = require("classnames");
+const _ = require("lodash");
class RulesConnectionsFilter extends Component {
- constructor(props) {
- super(props);
- this.state = {
- mounted: false,
- rules: [],
- activeRules: []
- };
-
- this.needRedirect = false;
- }
+ state = {
+ rules: [],
+ activeRules: []
+ };
componentDidMount() {
- let params = new URLSearchParams(this.props.location.search);
+ const params = new URLSearchParams(this.props.location.search);
let activeRules = params.getAll("matched_rules") || [];
- backend.get("/api/rules").then(res => {
- let rules = res.json.flatMap(rule => rule.enabled ? [{id: rule.id, name: rule.name}] : []);
- activeRules = rules.filter(rule => activeRules.some(id => rule.id === id));
- this.setState({rules, activeRules, mounted: true});
+ backend.get("/api/rules").then((res) => {
+ let rules = res.json.flatMap((rule) => rule.enabled ? [{id: rule.id, name: rule.name}] : []);
+ activeRules = rules.filter((rule) => activeRules.some((id) => rule.id === id));
+ this.setState({rules, activeRules});
});
- }
- componentDidUpdate(prevProps, prevState, snapshot) {
- let urlParams = new URLSearchParams(this.props.location.search);
- let externalRules = urlParams.getAll("matched_rules") || [];
- let activeRules = this.state.activeRules.map(r => r.id);
- let compareRules = (first, second) => first.sort().join(",") === second.sort().join(",");
- if (this.state.mounted &&
- compareRules(prevState.activeRules.map(r => r.id), activeRules) &&
- !compareRules(externalRules, activeRules)) {
- this.setState({activeRules: externalRules.map(id => this.state.rules.find(r => r.id === id))});
- }
+ this.connectionsFiltersCallback = (payload) => {
+ if ("matched_rules" in payload && !_.isEqual(payload["matched_rules"].sort(), this.state.activeRules.sort())) {
+ const newRules = this.state.rules.filter((r) => payload["matched_rules"].includes(r.id));
+ this.setState({
+ activeRules: newRules.map((r) => {
+ return {id: r.id, name: r.name};
+ })
+ });
+ }
+ };
+ dispatcher.register("connections_filters", this.connectionsFiltersCallback);
}
- onDelete(i) {
- const activeRules = this.state.activeRules.slice(0);
- activeRules.splice(i, 1);
- this.needRedirect = true;
- this.setState({ activeRules });
+ componentWillUnmount() {
+ dispatcher.unregister(this.connectionsFiltersCallback);
}
- onAddition(rule) {
- if (!this.state.activeRules.includes(rule)) {
- const activeRules = [].concat(this.state.activeRules, rule);
- this.needRedirect = true;
+ onChange = (activeRules) => {
+ if (!_.isEqual(activeRules.sort(), this.state.activeRules.sort())) {
this.setState({activeRules});
+ dispatcher.dispatch("connections_filters", {"matched_rules": activeRules.map((r) => r.id)});
}
- }
+ };
render() {
- let redirect = null;
-
- if (this.needRedirect) {
- let urlParams = new URLSearchParams(this.props.location.search);
- urlParams.delete("matched_rules");
- this.state.activeRules.forEach(rule => urlParams.append("matched_rules", rule.id));
- redirect = <Redirect push to={`${this.props.location.pathname}?${urlParams}`} />;
-
- this.needRedirect = false;
- }
-
return (
- <div className={classNames("filter", "d-inline-block", {"filter-active" : this.state.filterActive === "true"})}>
- <div className="filter-booleanq">
- <ReactTags tags={this.state.activeRules} suggestions={this.state.rules}
- onDelete={this.onDelete.bind(this)} onAddition={this.onAddition.bind(this)}
- minQueryLength={0} placeholderText="rule_name"
- suggestionsFilter={(suggestion, query) =>
- suggestion.name.startsWith(query) && !this.state.activeRules.includes(suggestion)} />
+ <div
+ className={classNames("filter", "d-inline-block", {"filter-active": this.state.filterActive === "true"})}>
+ <div className="filter-rules">
+ <TagField tags={this.state.activeRules} onChange={this.onChange}
+ suggestions={_.differenceWith(this.state.rules, this.state.activeRules, _.isEqual)}
+ minQueryLength={0} name="matched_rules" inline small placeholder="rule_name"/>
</div>
-
- {redirect}
</div>
);
}
diff --git a/frontend/src/components/filters/RulesConnectionsFilter.scss b/frontend/src/components/filters/RulesConnectionsFilter.scss
deleted file mode 100644
index 71efd0d..0000000
--- a/frontend/src/components/filters/RulesConnectionsFilter.scss
+++ /dev/null
@@ -1,118 +0,0 @@
-@import "../../colors";
-
-.react-tags {
- font-size: 12px;
- position: relative;
- z-index: 10;
- padding: 0 6px;
- cursor: text;
- border-radius: 4px;
- background-color: $color-primary-2;
-}
-
-.react-tags.is-focused {
- border-color: #b1b1b1;
-}
-
-.react-tags__selected {
- display: inline;
-}
-
-.react-tags__selected-tag {
- font-size: 11px;
- display: inline-block;
- margin: 0 6px 6px 0;
- padding: 2px 4px;
- color: $color-primary-3;
- border: none;
- border-radius: 2px;
- background: $color-primary-4;
-}
-
-.react-tags__selected-tag::after {
- margin-left: 8px;
- content: "\2715";
- color: $color-primary-3;
-}
-
-.react-tags__selected-tag:hover,
-.react-tags__selected-tag:focus {
- border-color: #b1b1b1;
-}
-
-.react-tags__search {
- display: inline-block;
- max-width: 100%;
- padding: 7px 10px;
-}
-
-@media screen and (min-width: 30em) {
- .react-tags__search {
- position: relative;
- }
-}
-
-.react-tags__search-input {
- font-size: inherit;
- line-height: inherit;
- max-width: 100%;
- margin: 0;
- padding: 0;
- color: $color-primary-4;
- border: 0;
- outline: none;
- background-color: $color-primary-2;
-}
-
-.react-tags__search-input::-ms-clear {
- display: none;
-}
-
-.react-tags__suggestions {
- position: absolute;
- top: 100%;
- left: 0;
- width: 100%;
-}
-
-@media screen and (min-width: 30em) {
- .react-tags__suggestions {
- width: 240px;
- }
-}
-
-.react-tags__suggestions ul {
- font-size: 12px;
- margin: 4px -1px;
- padding: 0;
- list-style: none;
- color: $color-primary-1;
- border-radius: 2px;
- background: $color-primary-4;
-}
-
-.react-tags__suggestions li {
- padding: 3px 5px;
- border-bottom: 1px solid #ddd;
-}
-
-.react-tags__suggestions li mark {
- font-weight: 600;
- text-decoration: underline;
- background: none;
-}
-
-.react-tags__suggestions li:hover {
- cursor: pointer;
- color: $color-primary-4;
- background: $color-primary-0;
-}
-
-.react-tags__suggestions li.is-active {
- background: #b7cfe0;
-}
-
-.react-tags__suggestions li.is-disabled {
- cursor: auto;
- opacity: 0.5;
-}
diff --git a/frontend/src/components/filters/StringConnectionsFilter.js b/frontend/src/components/filters/StringConnectionsFilter.js
index f463593..c5d7075 100644
--- a/frontend/src/components/filters/StringConnectionsFilter.js
+++ b/frontend/src/components/filters/StringConnectionsFilter.js
@@ -1,36 +1,52 @@
-import React, {Component} from 'react';
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import React, {Component} from "react";
import {withRouter} from "react-router-dom";
-import {Redirect} from "react-router";
+import dispatcher from "../../dispatcher";
import InputField from "../fields/InputField";
class StringConnectionsFilter extends Component {
- constructor(props) {
- super(props);
- this.state = {
- fieldValue: "",
- filterValue: null,
- timeoutHandle: null,
- invalidValue: false
- };
- this.needRedirect = false;
- this.filterChanged = this.filterChanged.bind(this);
- }
+ state = {
+ fieldValue: "",
+ filterValue: null,
+ timeoutHandle: null,
+ invalidValue: false
+ };
componentDidMount() {
let params = new URLSearchParams(this.props.location.search);
this.updateStateFromFilterValue(params.get(this.props.filterName));
+
+ this.connectionsFiltersCallback = (payload) => {
+ const name = this.props.filterName;
+ if (name in payload && this.state.filterValue !== payload[name]) {
+ this.updateStateFromFilterValue(payload[name]);
+ }
+ };
+ dispatcher.register("connections_filters", this.connectionsFiltersCallback);
}
- componentDidUpdate(prevProps, prevState, snapshot) {
- let urlParams = new URLSearchParams(this.props.location.search);
- let filterValue = urlParams.get(this.props.filterName);
- if (prevState.filterValue === this.state.filterValue && this.state.filterValue !== filterValue) {
- this.updateStateFromFilterValue(filterValue);
- }
+ componentWillUnmount() {
+ dispatcher.unregister(this.connectionsFiltersCallback);
}
- updateStateFromFilterValue(filterValue) {
+ updateStateFromFilterValue = (filterValue) => {
if (filterValue !== null) {
let fieldValue = filterValue;
if (typeof this.props.decodeFunc === "function") {
@@ -40,28 +56,28 @@ class StringConnectionsFilter extends Component {
fieldValue = this.props.replaceFunc(fieldValue);
}
if (this.isValueValid(fieldValue)) {
- this.setState({
- fieldValue: fieldValue,
- filterValue: filterValue
- });
+ this.setState({fieldValue, filterValue});
} else {
- this.setState({
- fieldValue: fieldValue,
- invalidValue: true
- });
+ this.setState({fieldValue, invalidValue: true});
}
} else {
this.setState({fieldValue: "", filterValue: null});
}
- }
+ };
- isValueValid(value) {
+ isValueValid = (value) => {
return typeof this.props.validateFunc !== "function" ||
(typeof this.props.validateFunc === "function" && this.props.validateFunc(value));
- }
+ };
- filterChanged(fieldValue) {
- if (this.state.timeoutHandle !== null) {
+ changeFilterValue = (value) => {
+ const urlParams = {};
+ urlParams[this.props.filterName] = value;
+ dispatcher.dispatch("connections_filters", urlParams);
+ };
+
+ filterChanged = (fieldValue) => {
+ if (this.state.timeoutHandle) {
clearTimeout(this.state.timeoutHandle);
}
@@ -70,11 +86,11 @@ class StringConnectionsFilter extends Component {
}
if (fieldValue === "") {
- this.needRedirect = true;
this.setState({fieldValue: "", filterValue: null, invalidValue: false});
- return;
+ return this.changeFilterValue(null);
}
+
if (this.isValueValid(fieldValue)) {
let filterValue = fieldValue;
if (filterValue !== "" && typeof this.props.encodeFunc === "function") {
@@ -82,42 +98,26 @@ class StringConnectionsFilter extends Component {
}
this.setState({
- fieldValue: fieldValue,
+ fieldValue,
timeoutHandle: setTimeout(() => {
- this.needRedirect = true;
- this.setState({filterValue: filterValue});
+ this.setState({filterValue});
+ this.changeFilterValue(filterValue);
}, 500),
invalidValue: false
});
} else {
- this.needRedirect = true;
- this.setState({
- fieldValue: fieldValue,
- invalidValue: true
- });
+ this.setState({fieldValue, invalidValue: true});
}
- }
+ };
render() {
- let redirect = null;
- if (this.needRedirect) {
- let urlParams = new URLSearchParams(this.props.location.search);
- if (this.state.filterValue !== null) {
- urlParams.set(this.props.filterName, this.state.filterValue);
- } else {
- urlParams.delete(this.props.filterName);
- }
- redirect = <Redirect push to={`${this.props.location.pathname}?${urlParams}`} />;
- this.needRedirect = false;
- }
let active = this.state.filterValue !== null;
return (
<div className="filter" style={{"width": `${this.props.width}px`}}>
<InputField active={active} invalid={this.state.invalidValue} name={this.props.filterName}
placeholder={this.props.defaultFilterValue} onChange={this.filterChanged}
- value={this.state.fieldValue} inline={true} small={true} />
- {redirect}
+ value={this.state.fieldValue} inline={this.props.inline} small={this.props.small}/>
</div>
);
}
diff --git a/frontend/src/components/objects/Connection.js b/frontend/src/components/objects/Connection.js
new file mode 100644
index 0000000..b70b7f7
--- /dev/null
+++ b/frontend/src/components/objects/Connection.js
@@ -0,0 +1,114 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import React, {Component} from "react";
+import {Form} from "react-bootstrap";
+import backend from "../../backend";
+import dispatcher from "../../dispatcher";
+import {dateTimeToTime, durationBetween, formatSize} from "../../utils";
+import ButtonField from "../fields/ButtonField";
+import "./Connection.scss";
+import CopyLinkPopover from "./CopyLinkPopover";
+import LinkPopover from "./LinkPopover";
+
+const classNames = require("classnames");
+
+class Connection extends Component {
+
+ state = {
+ update: false
+ };
+
+ handleAction = (name) => {
+ if (name === "hide") {
+ const enabled = !this.props.data.hidden;
+ backend.post(`/api/connections/${this.props.data.id}/${enabled ? "hide" : "show"}`)
+ .then((_) => {
+ this.props.onEnabled(!enabled);
+ this.setState({update: true});
+ });
+ }
+ if (name === "mark") {
+ const marked = this.props.data.marked;
+ backend.post(`/api/connections/${this.props.data.id}/${marked ? "unmark" : "mark"}`)
+ .then((_) => {
+ this.props.onMarked(!marked);
+ this.setState({update: true});
+ });
+ }
+ };
+
+ render() {
+ let conn = this.props.data;
+ let serviceName = "/dev/null";
+ let serviceColor = "#0f192e";
+ if (this.props.services[conn["port_dst"]]) {
+ const service = this.props.services[conn["port_dst"]];
+ serviceName = service.name;
+ serviceColor = service.color;
+ }
+ let startedAt = new Date(conn["started_at"]);
+ let closedAt = new Date(conn["closed_at"]);
+ let processedAt = new Date(conn["processed_at"]);
+ let timeInfo = <div>
+ <span>Started at {startedAt.toLocaleDateString() + " " + startedAt.toLocaleTimeString()}</span><br/>
+ <span>Processed at {processedAt.toLocaleDateString() + " " + processedAt.toLocaleTimeString()}</span><br/>
+ <span>Closed at {closedAt.toLocaleDateString() + " " + closedAt.toLocaleTimeString()}</span>
+ </div>;
+
+ const commentPopoverContent = <div>
+ <span>Click to <strong>{conn.comment.length > 0 ? "edit" : "add"}</strong> comment</span>
+ {conn.comment && <Form.Control as="textarea" readOnly={true} rows={2} defaultValue={conn.comment}/>}
+ </div>;
+
+ return (
+ <tr className={classNames("connection", {"connection-selected": this.props.selected},
+ {"has-matched-rules": conn.matched_rules.length > 0})}>
+ <td>
+ <span className="connection-service">
+ <ButtonField small fullSpan color={serviceColor} name={serviceName}
+ onClick={() => dispatcher.dispatch("connections_filters",
+ {"service_port": conn["port_dst"].toString()})}/>
+ </span>
+ </td>
+ <td className="clickable" onClick={this.props.onSelected}>{conn["ip_src"]}</td>
+ <td className="clickable" onClick={this.props.onSelected}>{conn["port_src"]}</td>
+ <td className="clickable" onClick={this.props.onSelected}>{conn["ip_dst"]}</td>
+ <td className="clickable" onClick={this.props.onSelected}>{conn["port_dst"]}</td>
+ <td className="clickable" onClick={this.props.onSelected}>
+ <LinkPopover text={dateTimeToTime(conn["started_at"])} content={timeInfo} placement="right"/>
+ </td>
+ <td className="clickable" onClick={this.props.onSelected}>{durationBetween(startedAt, closedAt)}</td>
+ <td className="clickable" onClick={this.props.onSelected}>{formatSize(conn["client_bytes"])}</td>
+ <td className="clickable" onClick={this.props.onSelected}>{formatSize(conn["server_bytes"])}</td>
+ <td className="connection-actions">
+ <LinkPopover text={<span className={classNames("connection-icon", {"icon-enabled": conn.marked})}
+ onClick={() => this.handleAction("mark")}>!!</span>}
+ content={<span>Mark this connection</span>} placement="right"/>
+ <LinkPopover text={<span className={classNames("connection-icon", {"icon-enabled": conn.comment})}
+ onClick={() => this.handleAction("comment")}>@</span>}
+ content={commentPopoverContent} placement="right"/>
+ <CopyLinkPopover text="#" value={conn.id}
+ textClassName={classNames("connection-icon", {"icon-enabled": conn.hidden})}/>
+ </td>
+ </tr>
+ );
+ }
+
+}
+
+export default Connection;
diff --git a/frontend/src/components/Connection.scss b/frontend/src/components/objects/Connection.scss
index cb7fa54..bf66272 100644
--- a/frontend/src/components/Connection.scss
+++ b/frontend/src/components/objects/Connection.scss
@@ -1,4 +1,4 @@
-@import "../colors.scss";
+@import "../../colors";
.connection {
border-top: 3px solid $color-primary-3;
@@ -42,6 +42,14 @@
&.has-matched-rules {
border-bottom: 0;
}
+
+ .link-popover {
+ font-weight: 400;
+ }
+
+ .connection-actions .link-popover {
+ text-decoration: none;
+ }
}
.connection-popover {
diff --git a/frontend/src/components/objects/ConnectionMatchedRules.js b/frontend/src/components/objects/ConnectionMatchedRules.js
new file mode 100644
index 0000000..a69cad8
--- /dev/null
+++ b/frontend/src/components/objects/ConnectionMatchedRules.js
@@ -0,0 +1,51 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import React, {Component} from "react";
+import {withRouter} from "react-router-dom";
+import dispatcher from "../../dispatcher";
+import ButtonField from "../fields/ButtonField";
+import "./ConnectionMatchedRules.scss";
+
+class ConnectionMatchedRules extends Component {
+
+ onMatchedRulesSelected = (id) => {
+ const params = new URLSearchParams(this.props.location.search);
+ const rules = params.getAll("matched_rules");
+ if (!rules.includes(id)) {
+ rules.push(id);
+ dispatcher.dispatch("connections_filters", {"matched_rules": rules});
+ }
+ };
+
+ render() {
+ const matchedRules = this.props.matchedRules.map((mr) => {
+ const rule = this.props.rules.find((r) => r.id === mr);
+ return <ButtonField key={mr} onClick={() => this.onMatchedRulesSelected(rule.id)} name={rule.name}
+ color={rule.color} small/>;
+ });
+
+ return (
+ <tr className="connection-matches">
+ <td className="row-label">matched_rules:</td>
+ <td className="rule-buttons" colSpan={9}>{matchedRules}</td>
+ </tr>
+ );
+ }
+}
+
+export default withRouter(ConnectionMatchedRules);
diff --git a/frontend/src/components/ConnectionMatchedRules.scss b/frontend/src/components/objects/ConnectionMatchedRules.scss
index 65d9ac8..f46a914 100644
--- a/frontend/src/components/ConnectionMatchedRules.scss
+++ b/frontend/src/components/objects/ConnectionMatchedRules.scss
@@ -1,4 +1,4 @@
-@import "../colors.scss";
+@import "../../colors";
.connection-matches {
background-color: $color-primary-0;
diff --git a/frontend/src/components/objects/CopyLinkPopover.js b/frontend/src/components/objects/CopyLinkPopover.js
new file mode 100644
index 0000000..b951603
--- /dev/null
+++ b/frontend/src/components/objects/CopyLinkPopover.js
@@ -0,0 +1,54 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import React, {Component} from "react";
+import TextField from "../fields/TextField";
+import LinkPopover from "./LinkPopover";
+
+class CopyLinkPopover extends Component {
+
+ state = {};
+
+ constructor(props) {
+ super(props);
+
+ this.copyTextarea = React.createRef();
+ }
+
+ handleClick = () => {
+ this.copyTextarea.current.select();
+ document.execCommand("copy");
+ this.setState({copiedMessage: true});
+ setTimeout(() => this.setState({copiedMessage: false}), 3000);
+ };
+
+ render() {
+ const copyPopoverContent = <div style={{"width": "250px"}}>
+ {this.state.copiedMessage ? <span><strong>Copied!</strong></span> :
+ <span>Click to <strong>copy</strong></span>}
+ <TextField readonly rows={2} value={this.props.value} textRef={this.copyTextarea}/>
+ </div>;
+
+ return (
+ <LinkPopover text={<span className={this.props.textClassName}
+ onClick={this.handleClick}>{this.props.text}</span>}
+ content={copyPopoverContent} placement="right"/>
+ );
+ }
+}
+
+export default CopyLinkPopover;
diff --git a/frontend/src/components/objects/LinkPopover.js b/frontend/src/components/objects/LinkPopover.js
index 8768caa..551a819 100644
--- a/frontend/src/components/objects/LinkPopover.js
+++ b/frontend/src/components/objects/LinkPopover.js
@@ -1,7 +1,24 @@
-import React, {Component} from 'react';
-import {randomClassName} from "../../utils";
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import React, {Component} from "react";
import {OverlayTrigger, Popover} from "react-bootstrap";
-import './LinkPopover.scss';
+import {randomClassName} from "../../utils";
+import "./LinkPopover.scss";
class LinkPopover extends Component {
@@ -22,10 +39,11 @@ class LinkPopover extends Component {
);
return (this.props.content ?
- <OverlayTrigger trigger={["hover", "focus"]} placement={this.props.placement || "top"} overlay={popover}>
- <span className="link-popover">{this.props.text}</span>
- </OverlayTrigger> :
- <span className="link-popover-empty">{this.props.text}</span>
+ <OverlayTrigger trigger={["hover", "focus"]} placement={this.props.placement || "top"}
+ overlay={popover}>
+ <span className="link-popover">{this.props.text}</span>
+ </OverlayTrigger> :
+ <span className="link-popover-empty">{this.props.text}</span>
);
}
}
diff --git a/frontend/src/components/objects/LinkPopover.scss b/frontend/src/components/objects/LinkPopover.scss
index 725224c..c81f8bb 100644
--- a/frontend/src/components/objects/LinkPopover.scss
+++ b/frontend/src/components/objects/LinkPopover.scss
@@ -5,3 +5,8 @@
cursor: pointer;
text-decoration: underline;
}
+
+.popover {
+ font-family: "Fira Code", monospace;
+ font-size: 0.75em;
+}
diff --git a/frontend/src/components/MessageAction.js b/frontend/src/components/objects/MessageAction.js
index 8f4b031..e0c96e8 100644
--- a/frontend/src/components/MessageAction.js
+++ b/frontend/src/components/objects/MessageAction.js
@@ -1,8 +1,25 @@
-import React, {Component} from 'react';
-import './MessageAction.scss';
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import React, {Component} from "react";
import {Modal} from "react-bootstrap";
-import TextField from "./fields/TextField";
-import ButtonField from "./fields/ButtonField";
+import ButtonField from "../fields/ButtonField";
+import TextField from "../fields/TextField";
+import "./MessageAction.scss";
class MessageAction extends Component {
@@ -17,7 +34,7 @@ class MessageAction extends Component {
copyActionValue() {
this.actionValue.current.select();
- document.execCommand('copy');
+ document.execCommand("copy");
this.setState({copyButtonText: "copied!"});
setTimeout(() => this.setState({copyButtonText: "copy"}), 3000);
}
@@ -26,7 +43,7 @@ class MessageAction extends Component {
return (
<Modal
{...this.props}
- show="true"
+ show={true}
size="lg"
aria-labelledby="message-action-dialog"
centered
@@ -37,11 +54,12 @@ class MessageAction extends Component {
</Modal.Title>
</Modal.Header>
<Modal.Body>
- <TextField readonly value={this.props.actionValue} textRef={this.actionValue} rows={15} />
+ <TextField readonly value={this.props.actionValue} textRef={this.actionValue} rows={15}/>
</Modal.Body>
<Modal.Footer className="dialog-footer">
- <ButtonField variant="green" bordered onClick={this.copyActionValue} name={this.state.copyButtonText} />
- <ButtonField variant="red" bordered onClick={this.props.onHide} name="close" />
+ <ButtonField variant="green" bordered onClick={this.copyActionValue}
+ name={this.state.copyButtonText}/>
+ <ButtonField variant="red" bordered onClick={this.props.onHide} name="close"/>
</Modal.Footer>
</Modal>
);
diff --git a/frontend/src/components/MessageAction.scss b/frontend/src/components/objects/MessageAction.scss
index faa23d3..996007b 100644
--- a/frontend/src/components/MessageAction.scss
+++ b/frontend/src/components/objects/MessageAction.scss
@@ -1,4 +1,4 @@
-@import "../colors.scss";
+@import "../../colors";
.message-action-value {
font-size: 13px;
diff --git a/frontend/src/components/panels/ConfigurationPane.js b/frontend/src/components/pages/ConfigurationPage.js
index 10309f6..8f9b68b 100644
--- a/frontend/src/components/panels/ConfigurationPane.js
+++ b/frontend/src/components/pages/ConfigurationPage.js
@@ -1,18 +1,37 @@
-import React, {Component} from 'react';
-import './common.scss';
-import './ConfigurationPane.scss';
-import LinkPopover from "../objects/LinkPopover";
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import React, {Component} from "react";
import {Col, Container, Row} from "react-bootstrap";
-import InputField from "../fields/InputField";
-import TextField from "../fields/TextField";
-import ButtonField from "../fields/ButtonField";
-import CheckField from "../fields/CheckField";
-import {createCurlCommand} from "../../utils";
import Table from "react-bootstrap/Table";
-import validation from "../../validation";
import backend from "../../backend";
+import {createCurlCommand} from "../../utils";
+import validation from "../../validation";
+import ButtonField from "../fields/ButtonField";
+import CheckField from "../fields/CheckField";
+import InputField from "../fields/InputField";
+import TextField from "../fields/TextField";
+import Header from "../Header";
+import LinkPopover from "../objects/LinkPopover";
+import "../panels/common.scss";
+import "./common.scss";
+import "./ConfigurationPage.scss";
-class ConfigurationPane extends Component {
+class ConfigurationPage extends Component {
constructor(props) {
super(props);
@@ -23,8 +42,7 @@ class ConfigurationPane extends Component {
"flag_regex": "",
"auth_required": false
},
- "accounts": {
- }
+ "accounts": {}
},
newUsername: "",
newPassword: ""
@@ -33,9 +51,9 @@ class ConfigurationPane extends Component {
saveSettings = () => {
if (this.validateSettings(this.state.settings)) {
- backend.post("/setup", this.state.settings).then(_ => {
+ backend.post("/setup", this.state.settings).then((_) => {
this.props.onConfigured();
- }).catch(res => {
+ }).catch((res) => {
this.setState({setupStatusCode: res.status, setupResponse: JSON.stringify(res.json)});
});
}
@@ -43,11 +61,11 @@ class ConfigurationPane extends Component {
validateSettings = (settings) => {
let valid = true;
- if (!validation.isValidAddress(settings.config.server_address, true)) {
+ if (!validation.isValidAddress(settings.config["server_address"], true)) {
this.setState({serverAddressError: "invalid ip_address"});
valid = false;
}
- if (settings.config.flag_regex.length < 8) {
+ if (settings.config["flag_regex"].length < 8) {
this.setState({flagRegexError: "flag_regex.length < 8"});
valid = false;
}
@@ -68,7 +86,7 @@ class ConfigurationPane extends Component {
this.setState({
newUsername: "",
newPassword: "",
- settings: settings
+ settings
});
} else {
this.setState({
@@ -85,42 +103,46 @@ class ConfigurationPane extends Component {
const accounts = Object.entries(settings.accounts).map(([username, password]) =>
<tr key={username}>
<td>{username}</td>
- <td><LinkPopover text="******" content={password} /></td>
+ <td><LinkPopover text="******" content={password}/></td>
<td><ButtonField variant="red" small rounded name="delete"
- onClick={() => this.updateParam((s) => delete s.accounts[username]) }/></td>
+ onClick={() => this.updateParam((s) => delete s.accounts[username])}/></td>
</tr>).concat(<tr key={"new_account"}>
<td><InputField value={this.state.newUsername} small active={this.state.newUsernameActive}
- onChange={(v) => this.setState({newUsername: v})} /></td>
+ onChange={(v) => this.setState({newUsername: v})}/></td>
<td><InputField value={this.state.newPassword} small active={this.state.newPasswordActive}
- onChange={(v) => this.setState({newPassword: v})} /></td>
+ onChange={(v) => this.setState({newPassword: v})}/></td>
<td><ButtonField variant="green" small rounded name="add" onClick={this.addAccount}/></td>
</tr>);
return (
- <div className="configuration-pane">
- <div className="pane">
- <div className="pane-container">
+ <div className="page configuration-page">
+ <div className="page-header">
+ <Header />
+ </div>
+
+ <div className="page-content">
+ <div className="pane-container configuration-pane">
<div className="pane-section">
<div className="section-header">
<span className="api-request">POST /setup</span>
<span className="api-response"><LinkPopover text={this.state.setupStatusCode}
content={this.state.setupResponse}
- placement="left" /></span>
+ placement="left"/></span>
</div>
<div className="section-content">
<Container className="p-0">
<Row>
<Col>
- <InputField name="server_address" value={settings.config.server_address}
+ <InputField name="server_address" value={settings.config["server_address"]}
error={this.state.serverAddressError}
- onChange={(v) => this.updateParam((s) => s.config.server_address = v)} />
- <InputField name="flag_regex" value={settings.config.flag_regex}
- onChange={(v) => this.updateParam((s) => s.config.flag_regex = v)}
- error={this.state.flagRegexError} />
+ onChange={(v) => this.updateParam((s) => s.config["server_address"] = v)}/>
+ <InputField name="flag_regex" value={settings.config["flag_regex"]}
+ onChange={(v) => this.updateParam((s) => s.config["flag_regex"] = v)}
+ error={this.state.flagRegexError}/>
<div style={{"marginTop": "10px"}}>
- <CheckField checked={settings.config.auth_required} name="auth_required"
- onChange={(v) => this.updateParam((s) => s.config.auth_required = v)}/>
+ <CheckField checked={settings.config["auth_required"]} name="auth_required"
+ onChange={(v) => this.updateParam((s) => s.config["auth_required"] = v)}/>
</div>
</Col>
@@ -149,7 +171,7 @@ class ConfigurationPane extends Component {
</div>
<div className="section-footer">
- <ButtonField variant="green" name="save" bordered onClick={this.saveSettings} />
+ <ButtonField variant="green" name="save" bordered onClick={this.saveSettings}/>
</div>
</div>
</div>
@@ -159,4 +181,4 @@ class ConfigurationPane extends Component {
}
}
-export default ConfigurationPane;
+export default ConfigurationPage;
diff --git a/frontend/src/components/pages/ConfigurationPage.scss b/frontend/src/components/pages/ConfigurationPage.scss
new file mode 100644
index 0000000..4254547
--- /dev/null
+++ b/frontend/src/components/pages/ConfigurationPage.scss
@@ -0,0 +1,30 @@
+@import "../../colors";
+
+.configuration-page {
+ background-color: $color-primary-0;
+
+ .header-title {
+ margin: 50px auto;
+ }
+
+ .configuration-pane {
+ display: flex;
+ justify-content: center;
+ height: 100%;
+ padding-top: 100px;
+
+ .section-content {
+ background-color: $color-primary-3;
+ margin-top: 15px;
+ }
+
+ .section-table table {
+ background-color: red !important;
+ }
+
+ .section-footer {
+ background-color: $color-primary-3;
+ }
+ }
+}
+
diff --git a/frontend/src/components/pages/MainPage.js b/frontend/src/components/pages/MainPage.js
new file mode 100644
index 0000000..c4dcd20
--- /dev/null
+++ b/frontend/src/components/pages/MainPage.js
@@ -0,0 +1,76 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import React, {Component} from "react";
+import {Route, Switch} from "react-router-dom";
+import Filters from "../dialogs/Filters";
+import Header from "../Header";
+import Connections from "../panels/ConnectionsPane";
+import MainPane from "../panels/MainPane";
+import PcapsPane from "../panels/PcapsPane";
+import RulesPane from "../panels/RulesPane";
+import SearchPane from "../panels/SearchPane";
+import ServicesPane from "../panels/ServicesPane";
+import StreamsPane from "../panels/StreamsPane";
+import Timeline from "../Timeline";
+import "./common.scss";
+import "./MainPage.scss";
+
+class MainPage extends Component {
+
+ state = {};
+
+ render() {
+ let modal;
+ if (this.state.filterWindowOpen) {
+ modal = <Filters onHide={() => this.setState({filterWindowOpen: false})}/>;
+ }
+
+ return (
+ <div className="page main-page">
+ <div className="page-header">
+ <Header onOpenFilters={() => this.setState({filterWindowOpen: true})} configured={true}/>
+ </div>
+
+ <div className="page-content">
+ <div className="pane connections-pane">
+ <Connections onSelected={(c) => this.setState({selectedConnection: c})}/>
+ </div>
+ <div className="pane details-pane">
+ <Switch>
+ <Route path="/searches" children={<SearchPane/>}/>
+ <Route path="/pcaps" children={<PcapsPane/>}/>
+ <Route path="/rules" children={<RulesPane/>}/>
+ <Route path="/services" children={<ServicesPane/>}/>
+ <Route exact path="/connections/:id"
+ children={<StreamsPane connection={this.state.selectedConnection}/>}/>
+ <Route children={<MainPane version={this.props.version}/>}/>
+ </Switch>
+ </div>
+
+ {modal}
+ </div>
+
+ <div className="page-footer">
+ <Timeline/>
+ </div>
+ </div>
+ );
+ }
+}
+
+export default MainPage;
diff --git a/frontend/src/components/pages/MainPage.scss b/frontend/src/components/pages/MainPage.scss
new file mode 100644
index 0000000..4ca54c0
--- /dev/null
+++ b/frontend/src/components/pages/MainPage.scss
@@ -0,0 +1,25 @@
+@import "../../colors";
+
+.main-page {
+ .page-content {
+ display: flex;
+ flex: 1;
+ padding: 0 15px;
+ background-color: $color-primary-2;
+
+ .connections-pane {
+ flex: 1 0;
+ margin-right: 7.5px;
+ }
+
+ .details-pane {
+ position: relative;
+ flex: 1 1;
+ margin-left: 7.5px;
+ }
+ }
+
+ .page-footer {
+ flex: 0;
+ }
+}
diff --git a/frontend/src/components/pages/ServiceUnavailablePage.js b/frontend/src/components/pages/ServiceUnavailablePage.js
new file mode 100644
index 0000000..deb4cf8
--- /dev/null
+++ b/frontend/src/components/pages/ServiceUnavailablePage.js
@@ -0,0 +1,34 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import React, {Component} from "react";
+import "./MainPage.scss";
+
+class ServiceUnavailablePage extends Component {
+
+ state = {};
+
+ render() {
+ return (
+ <div className="main-page">
+
+ </div>
+ );
+ }
+}
+
+export default ServiceUnavailablePage;
diff --git a/frontend/src/components/pages/common.scss b/frontend/src/components/pages/common.scss
new file mode 100644
index 0000000..fcf5c20
--- /dev/null
+++ b/frontend/src/components/pages/common.scss
@@ -0,0 +1,16 @@
+.page {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+
+ .page-header,
+ .page-footer {
+ flex: 0;
+ }
+
+ .page-content {
+ overflow: hidden;
+ flex: 1;
+ }
+}
diff --git a/frontend/src/components/panels/ConfigurationPane.scss b/frontend/src/components/panels/ConfigurationPane.scss
deleted file mode 100644
index ef48b34..0000000
--- a/frontend/src/components/panels/ConfigurationPane.scss
+++ /dev/null
@@ -1,18 +0,0 @@
-@import "../../colors";
-
-.configuration-pane {
- display: flex;
- align-items: center;
- justify-content: center;
- height: 100%;
- background-color: $color-primary-0;
-
- .pane {
- flex-basis: 900px;
- margin-bottom: 200px;
- }
-
- .pane-container {
- padding-bottom: 1px;
- }
-}
diff --git a/frontend/src/components/panels/ConnectionsPane.js b/frontend/src/components/panels/ConnectionsPane.js
new file mode 100644
index 0000000..6418b3e
--- /dev/null
+++ b/frontend/src/components/panels/ConnectionsPane.js
@@ -0,0 +1,310 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import React, {Component} from "react";
+import Table from "react-bootstrap/Table";
+import {Redirect} from "react-router";
+import {withRouter} from "react-router-dom";
+import backend from "../../backend";
+import dispatcher from "../../dispatcher";
+import log from "../../log";
+import {updateParams} from "../../utils";
+import ButtonField from "../fields/ButtonField";
+import Connection from "../objects/Connection";
+import ConnectionMatchedRules from "../objects/ConnectionMatchedRules";
+import "./ConnectionsPane.scss";
+
+const classNames = require("classnames");
+
+class ConnectionsPane extends Component {
+
+ state = {
+ loading: false,
+ connections: [],
+ firstConnection: null,
+ lastConnection: null,
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.scrollTopThreashold = 0.00001;
+ this.scrollBottomThreashold = 0.99999;
+ this.maxConnections = 200;
+ this.queryLimit = 50;
+ this.connectionsListRef = React.createRef();
+ this.lastScrollPosition = 0;
+ }
+
+ componentDidMount() {
+ let urlParams = new URLSearchParams(this.props.location.search);
+ this.setState({urlParams});
+
+ const additionalParams = {limit: this.queryLimit};
+
+ const match = this.props.location.pathname.match(/^\/connections\/([a-f0-9]{24})$/);
+ if (match != null) {
+ const id = match[1];
+ additionalParams.from = id;
+ backend.get(`/api/connections/${id}`)
+ .then((res) => this.connectionSelected(res.json))
+ .catch((error) => log.error("Error loading initial connection", error));
+ }
+
+ this.loadConnections(additionalParams, urlParams, true).then(() => log.debug("Connections loaded"));
+
+ dispatcher.register("connections_filters", this.handleConnectionsFilters);
+ dispatcher.register("timeline_updates", this.handleTimelineUpdates);
+ dispatcher.register("notifications", this.handleNotifications);
+ dispatcher.register("pulse_connections_view", this.handlePulseConnectionsView);
+ }
+
+ componentWillUnmount() {
+ dispatcher.unregister(this.handleConnectionsFilters);
+ dispatcher.unregister(this.handleTimelineUpdates);
+ dispatcher.unregister(this.handleNotifications);
+ dispatcher.unregister(this.handlePulseConnectionsView);
+ }
+
+ handleConnectionsFilters = (payload) => {
+ const newParams = updateParams(this.state.urlParams, payload);
+ if (this.state.urlParams.toString() === newParams.toString()) {
+ return;
+ }
+
+ log.debug("Update following url params:", payload);
+ this.queryStringRedirect = true;
+ this.setState({urlParams: newParams});
+
+ this.loadConnections({limit: this.queryLimit}, newParams)
+ .then(() => log.info("ConnectionsPane reloaded after query string update"));
+ };
+
+ handleTimelineUpdates = (payload) => {
+ this.connectionsListRef.current.scrollTop = 0;
+ this.loadConnections({
+ "started_after": Math.round(payload.from.getTime() / 1000),
+ "started_before": Math.round(payload.to.getTime() / 1000),
+ limit: this.maxConnections
+ }).then(() => log.info(`Loading connections between ${payload.from} and ${payload.to}`));
+ };
+
+ handleNotifications = (payload) => {
+ if (payload.event === "rules.new" || payload.event === "rules.edit") {
+ this.loadRules().then(() => log.debug("Loaded connection rules after notification update"));
+ }
+ if (payload.event === "services.edit") {
+ this.loadServices().then(() => log.debug("Services reloaded after notification update"));
+ }
+ };
+
+ handlePulseConnectionsView = (payload) => {
+ this.setState({pulseConnectionsView: true});
+ setTimeout(() => this.setState({pulseConnectionsView: false}), payload.duration);
+ };
+
+ connectionSelected = (c) => {
+ this.connectionSelectedRedirect = true;
+ this.setState({selected: c.id});
+ this.props.onSelected(c);
+ log.debug(`Connection ${c.id} selected`);
+ };
+
+ handleScroll = (e) => {
+ if (this.disableScrollHandler) {
+ this.lastScrollPosition = e.currentTarget.scrollTop;
+ return;
+ }
+
+ let relativeScroll = e.currentTarget.scrollTop / (e.currentTarget.scrollHeight - e.currentTarget.clientHeight);
+ if (!this.state.loading && relativeScroll > this.scrollBottomThreashold) {
+ this.loadConnections({from: this.state.lastConnection.id, limit: this.queryLimit,})
+ .then(() => log.info("Following connections loaded"));
+ }
+ if (!this.state.loading && relativeScroll < this.scrollTopThreashold) {
+ this.loadConnections({to: this.state.firstConnection.id, limit: this.queryLimit,})
+ .then(() => log.info("Previous connections loaded"));
+ if (this.state.showMoreRecentButton) {
+ this.setState({showMoreRecentButton: false});
+ }
+ } else {
+ if (this.lastScrollPosition > e.currentTarget.scrollTop) {
+ if (!this.state.showMoreRecentButton) {
+ this.setState({showMoreRecentButton: true});
+ }
+ } else {
+ if (this.state.showMoreRecentButton) {
+ this.setState({showMoreRecentButton: false});
+ }
+ }
+ }
+ this.lastScrollPosition = e.currentTarget.scrollTop;
+ };
+
+ async loadConnections(additionalParams, initialParams = null, isInitial = false) {
+ if (!initialParams) {
+ initialParams = this.state.urlParams;
+ }
+ const urlParams = new URLSearchParams(initialParams.toString());
+ for (const [name, value] of Object.entries(additionalParams)) {
+ urlParams.set(name, value);
+ }
+
+ this.setState({loading: true});
+ if (!this.state.rules) {
+ await this.loadRules();
+ }
+ if (!this.state.services) {
+ await this.loadServices();
+ }
+
+ let res = (await backend.get(`/api/connections?${urlParams}`)).json;
+
+ let connections = this.state.connections;
+ let firstConnection = this.state.firstConnection;
+ let lastConnection = this.state.lastConnection;
+
+ if (additionalParams && additionalParams.from && !additionalParams.to) {
+ if (res.length > 0) {
+ if (!isInitial) {
+ res = res.slice(1);
+ }
+ connections = this.state.connections.concat(res);
+ lastConnection = connections[connections.length - 1];
+ if (isInitial) {
+ firstConnection = connections[0];
+ }
+ if (connections.length > this.maxConnections) {
+ connections = connections.slice(connections.length - this.maxConnections,
+ connections.length - 1);
+ firstConnection = connections[0];
+ }
+ }
+ } else if (additionalParams && additionalParams.to && !additionalParams.from) {
+ if (res.length > 0) {
+ connections = res.slice(0, res.length - 1).concat(this.state.connections);
+ firstConnection = connections[0];
+ if (connections.length > this.maxConnections) {
+ connections = connections.slice(0, this.maxConnections);
+ lastConnection = connections[this.maxConnections - 1];
+ }
+ }
+ } else {
+ if (res.length > 0) {
+ connections = res;
+ firstConnection = connections[0];
+ lastConnection = connections[connections.length - 1];
+ } else {
+ connections = [];
+ firstConnection = null;
+ lastConnection = null;
+ }
+ }
+
+ this.setState({loading: false, connections, firstConnection, lastConnection});
+
+ if (firstConnection != null && lastConnection != null) {
+ dispatcher.dispatch("connection_updates", {
+ from: new Date(lastConnection["started_at"]),
+ to: new Date(firstConnection["started_at"])
+ });
+ }
+ }
+
+ loadRules = async () => {
+ return backend.get("/api/rules").then((res) => this.setState({rules: res.json}));
+ };
+
+ loadServices = async () => {
+ return backend.get("/api/services").then((res) => this.setState({services: res.json}));
+ };
+
+ render() {
+ let redirect;
+ if (this.connectionSelectedRedirect) {
+ redirect = <Redirect push to={`/connections/${this.state.selected}?${this.state.urlParams}`}/>;
+ this.connectionSelectedRedirect = false;
+ } else if (this.queryStringRedirect) {
+ redirect = <Redirect push to={`${this.props.location.pathname}?${this.state.urlParams}`}/>;
+ this.queryStringRedirect = false;
+ }
+
+ let loading = null;
+ if (this.state.loading) {
+ loading = <tr>
+ <td colSpan={10}>Loading...</td>
+ </tr>;
+ }
+
+ return (
+ <div className="connections-container">
+ {this.state.showMoreRecentButton && <div className="most-recent-button">
+ <ButtonField name="most_recent" variant="green" bordered onClick={() => {
+ this.disableScrollHandler = true;
+ this.connectionsListRef.current.scrollTop = 0;
+ this.loadConnections({limit: this.queryLimit})
+ .then(() => {
+ this.disableScrollHandler = false;
+ log.info("Most recent connections loaded");
+ });
+ }}/>
+ </div>}
+
+ <div className={classNames("connections", {"connections-pulse": this.state.pulseConnectionsView})}
+ onScroll={this.handleScroll} ref={this.connectionsListRef}>
+ <Table borderless size="sm">
+ <thead>
+ <tr>
+ <th>service</th>
+ <th>srcip</th>
+ <th>srcport</th>
+ <th>dstip</th>
+ <th>dstport</th>
+ <th>started_at</th>
+ <th>duration</th>
+ <th>up</th>
+ <th>down</th>
+ <th>actions</th>
+ </tr>
+ </thead>
+ <tbody>
+ {
+ this.state.connections.flatMap((c) => {
+ return [<Connection key={c.id} data={c} onSelected={() => this.connectionSelected(c)}
+ selected={this.state.selected === c.id}
+ onMarked={(marked) => c.marked = marked}
+ onEnabled={(enabled) => c.hidden = !enabled}
+ services={this.state.services}/>,
+ c.matched_rules.length > 0 &&
+ <ConnectionMatchedRules key={c.id + "_m"} matchedRules={c.matched_rules}
+ rules={this.state.rules}/>
+ ];
+ })
+ }
+ {loading}
+ </tbody>
+ </Table>
+
+ {redirect}
+ </div>
+ </div>
+ );
+ }
+
+}
+
+export default withRouter(ConnectionsPane);
diff --git a/frontend/src/components/panels/ConnectionsPane.scss b/frontend/src/components/panels/ConnectionsPane.scss
new file mode 100644
index 0000000..59fe372
--- /dev/null
+++ b/frontend/src/components/panels/ConnectionsPane.scss
@@ -0,0 +1,41 @@
+@import "../../colors";
+
+.connections-container {
+ position: relative;
+ height: 100%;
+ background-color: $color-primary-3;
+
+ .connections {
+ position: relative;
+ overflow-y: scroll;
+ height: 100%;
+
+ .table {
+ margin-bottom: 0;
+ }
+
+ th {
+ font-size: 13.5px;
+ position: sticky;
+ top: 0;
+ padding: 5px;
+ border: none;
+ background-color: $color-primary-3;
+ }
+
+ &:hover::-webkit-scrollbar-thumb {
+ background: $color-secondary-2;
+ }
+ }
+
+ .most-recent-button {
+ position: absolute;
+ z-index: 20;
+ top: 45px;
+ left: calc(50% - 50px);
+ }
+
+ .connections-pulse {
+ animation: pulse 2s infinite;
+ }
+}
diff --git a/frontend/src/components/panels/MainPane.js b/frontend/src/components/panels/MainPane.js
index 3202d6d..ce72be5 100644
--- a/frontend/src/components/panels/MainPane.js
+++ b/frontend/src/components/panels/MainPane.js
@@ -1,56 +1,112 @@
-import React, {Component} from 'react';
-import './common.scss';
-import './MainPane.scss';
-import Connections from "../../views/Connections";
-import ConnectionContent from "../ConnectionContent";
-import {Route, Switch, withRouter} from "react-router-dom";
-import PcapPane from "./PcapPane";
-import backend from "../../backend";
-import RulePane from "./RulePane";
-import ServicePane from "./ServicePane";
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import React, {Component} from "react";
+import Typed from "typed.js";
+import dispatcher from "../../dispatcher";
+import "./common.scss";
+import "./MainPane.scss";
+import PcapsPane from "./PcapsPane";
+import RulesPane from "./RulesPane";
+import ServicesPane from "./ServicesPane";
+import StreamsPane from "./StreamsPane";
class MainPane extends Component {
- constructor(props) {
- super(props);
- this.state = {
- selectedConnection: null,
- loading: false
+ state = {};
+
+ componentDidMount() {
+ const nl = "^600\n^400";
+ const options = {
+ strings: [
+ `welcome to caronte!^1000 the current version is ${this.props.version}` + nl +
+ "caronte is a network analyzer,^300 it is able to read pcaps and extract connections", // 0
+ "the left panel lists all connections that have already been closed" + nl +
+ "scrolling up the list will load the most recent connections,^300 downward the oldest ones", // 1
+ "by selecting a connection you can view its content,^300 which will be shown in the right panel" + nl +
+ "you can choose the display format,^300 or decide to download the connection content", // 2
+ "below there is the timeline,^300 which shows the number of connections per minute per service" + nl +
+ "you can use the sliding window to move the time range of the connections to be displayed", // 3
+ "there are also additional metrics,^300 selectable from the drop-down menu", // 4
+ "at the top are the filters,^300 which can be used to select only certain types of connections" + nl +
+ "you can choose which filters to display in the top bar from the filters window", // 5
+ "in the pcaps panel it is possible to analyze new pcaps,^300 or to see the pcaps already analyzed" + nl +
+ "you can load pcaps from your browser,^300 or process pcaps already present on the filesystem", // 6
+ "in the rules panel you can see the rules already created,^300 or create new ones" + nl +
+ "the rules inserted will be used only to label new connections, not those already analyzed" + nl +
+ "a connection is tagged if it meets all the requirements specified by the rule", // 7
+ "in the services panel you can assign new services or edit existing ones" + nl +
+ "each service is associated with a port number,^300 and will be shown in the connection list", // 8
+ "from the configuration panel you can change the settings of the frontend application", // 9
+ "that's all! and have fun!" + nl + "created by @eciavatta" // 10
+ ],
+ typeSpeed: 40,
+ cursorChar: "_",
+ backSpeed: 5,
+ smartBackspace: false,
+ backDelay: 1500,
+ preStringTyped: (arrayPos) => {
+ switch (arrayPos) {
+ case 1:
+ return dispatcher.dispatch("pulse_connections_view", {duration: 12000});
+ case 2:
+ return this.setState({backgroundPane: <StreamsPane/>});
+ case 3:
+ this.setState({backgroundPane: null});
+ return dispatcher.dispatch("pulse_timeline", {duration: 12000});
+ case 6:
+ return this.setState({backgroundPane: <PcapsPane/>});
+ case 7:
+ return this.setState({backgroundPane: <RulesPane/>});
+ case 8:
+ return this.setState({backgroundPane: <ServicesPane/>});
+ case 10:
+ return this.setState({backgroundPane: null});
+ default:
+ return;
+ }
+ },
};
+ this.typed = new Typed(this.el, options);
}
- componentDidMount() {
- const match = this.props.location.pathname.match(/^\/connections\/([a-f0-9]{24})$/);
- if (match != null) {
- this.setState({loading: true});
- backend.get(`/api/connections/${match[1]}`)
- .then(res => this.setState({selectedConnection: res.json, loading: false}))
- .catch(error => console.log(error));
- }
+ componentWillUnmount() {
+ this.typed.destroy();
}
render() {
return (
- <div className="main-pane">
- <div className="pane connections-pane">
- {
- !this.state.loading &&
- <Connections onSelected={(c) => this.setState({selectedConnection: c})}
- initialConnection={this.state.selectedConnection} />
- }
+ <div className="pane-container">
+ <div className="main-pane">
+ <div className="pane-section">
+ <div className="tutorial">
+ <span style={{whiteSpace: "pre"}} ref={(el) => {
+ this.el = el;
+ }}/>
+ </div>
+ </div>
</div>
- <div className="pane details-pane">
- <Switch>
- <Route path="/pcaps" children={<PcapPane />} />
- <Route path="/rules" children={<RulePane />} />
- <Route path="/services" children={<ServicePane />} />
- <Route exact path="/connections/:id" children={<ConnectionContent connection={this.state.selectedConnection} />} />
- <Route children={<ConnectionContent />} />
- </Switch>
+ <div className="background-pane">
+ {this.state.backgroundPane}
</div>
</div>
);
}
+
}
-export default withRouter(MainPane);
+export default MainPane;
diff --git a/frontend/src/components/panels/MainPane.scss b/frontend/src/components/panels/MainPane.scss
index 2973c00..8f99b3c 100644
--- a/frontend/src/components/panels/MainPane.scss
+++ b/frontend/src/components/panels/MainPane.scss
@@ -1,22 +1,30 @@
@import "../../colors";
-.main-pane {
- display: flex;
- height: 100%;
- padding: 0 15px;
- background-color: $color-primary-2;
+.pane-container {
+ background-color: $color-primary-0;
- .pane {
- flex: 1;
- }
+ .main-pane {
+ position: absolute;
+ z-index: 50;
+ top: 0;
+ left: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+ background-color: transparent;
- .connections-pane {
- flex: 1 0;
- margin-right: 7.5px;
+ .tutorial {
+ flex-basis: 100%;
+ padding: 5px 10px;
+ text-align: center;
+ background-color: $color-primary-2;
+ }
}
- .details-pane {
- flex: 1 1;
- margin-left: 7.5px;
+ .background-pane {
+ height: 100%;
+ opacity: 0.4;
}
}
diff --git a/frontend/src/components/panels/PcapPane.js b/frontend/src/components/panels/PcapsPane.js
index 7b3fde6..b7d5ce9 100644
--- a/frontend/src/components/panels/PcapPane.js
+++ b/frontend/src/components/panels/PcapsPane.js
@@ -1,41 +1,68 @@
-import React, {Component} from 'react';
-import './PcapPane.scss';
-import './common.scss';
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import React, {Component} from "react";
import Table from "react-bootstrap/Table";
import backend from "../../backend";
+import dispatcher from "../../dispatcher";
import {createCurlCommand, dateTimeToTime, durationBetween, formatSize} from "../../utils";
-import InputField from "../fields/InputField";
+import ButtonField from "../fields/ButtonField";
import CheckField from "../fields/CheckField";
+import InputField from "../fields/InputField";
import TextField from "../fields/TextField";
-import ButtonField from "../fields/ButtonField";
+import CopyLinkPopover from "../objects/CopyLinkPopover";
import LinkPopover from "../objects/LinkPopover";
+import "./common.scss";
+import "./PcapsPane.scss";
-class PcapPane extends Component {
-
- constructor(props) {
- super(props);
+class PcapsPane extends Component {
- this.state = {
- sessions: [],
- isUploadFileValid: true,
- isUploadFileFocused: false,
- uploadFlushAll: false,
- isFileValid: true,
- isFileFocused: false,
- fileValue: "",
- processFlushAll: false,
- deleteOriginalFile: false
- };
- }
+ state = {
+ sessions: [],
+ isUploadFileValid: true,
+ isUploadFileFocused: false,
+ uploadFlushAll: false,
+ isFileValid: true,
+ isFileFocused: false,
+ fileValue: "",
+ processFlushAll: false,
+ deleteOriginalFile: false
+ };
componentDidMount() {
this.loadSessions();
+ dispatcher.register("notifications", this.handleNotifications);
+ document.title = "caronte:~/pcaps$";
}
+ componentWillUnmount() {
+ dispatcher.unregister(this.handleNotifications);
+ }
+
+ handleNotifications = (payload) => {
+ if (payload.event.startsWith("pcap")) {
+ this.loadSessions();
+ }
+ };
+
loadSessions = () => {
backend.get("/api/pcap/sessions")
- .then(res => this.setState({sessions: res.json, sessionsStatusCode: res.status}))
- .catch(res => this.setState({
+ .then((res) => this.setState({sessions: res.json, sessionsStatusCode: res.status}))
+ .catch((res) => this.setState({
sessions: res.json, sessionsStatusCode: res.status,
sessionsResponse: JSON.stringify(res.json)
}));
@@ -50,14 +77,14 @@ class PcapPane extends Component {
const formData = new FormData();
formData.append("file", this.state.uploadSelectedFile);
formData.append("flush_all", this.state.uploadFlushAll);
- backend.postFile("/api/pcap/upload", formData).then(res => {
+ backend.postFile("/api/pcap/upload", formData).then((res) => {
this.setState({
uploadStatusCode: res.status,
uploadResponse: JSON.stringify(res.json)
});
this.resetUpload();
this.loadSessions();
- }).catch(res => this.setState({
+ }).catch((res) => this.setState({
uploadStatusCode: res.status,
uploadResponse: JSON.stringify(res.json)
})
@@ -71,17 +98,17 @@ class PcapPane extends Component {
}
backend.post("/api/pcap/file", {
- file: this.state.fileValue,
- flush_all: this.state.processFlushAll,
- delete_original_file: this.state.deleteOriginalFile
- }).then(res => {
+ "file": this.state.fileValue,
+ "flush_all": this.state.processFlushAll,
+ "delete_original_file": this.state.deleteOriginalFile
+ }).then((res) => {
this.setState({
processStatusCode: res.status,
processResponse: JSON.stringify(res.json)
});
this.resetProcess();
this.loadSessions();
- }).catch(res => this.setState({
+ }).catch((res) => this.setState({
processStatusCode: res.status,
processResponse: JSON.stringify(res.json)
})
@@ -108,10 +135,19 @@ class PcapPane extends Component {
};
render() {
- let sessions = this.state.sessions.map(s =>
- <tr key={s.id} className="table-row">
- <td>{s["id"].substring(0, 8)}</td>
- <td>{dateTimeToTime(s["started_at"])}</td>
+ let sessions = this.state.sessions.map((s) => {
+ const startedAt = new Date(s["started_at"]);
+ const completedAt = new Date(s["completed_at"]);
+ let timeInfo = <div>
+ <span>Started at {startedAt.toLocaleDateString() + " " + startedAt.toLocaleTimeString()}</span><br/>
+ <span>Completed at {completedAt.toLocaleDateString() + " " + completedAt.toLocaleTimeString()}</span>
+ </div>;
+
+ return <tr key={s.id} className="row-small row-clickable">
+ <td><CopyLinkPopover text={s["id"].substring(0, 8)} value={s["id"]}/></td>
+ <td>
+ <LinkPopover text={dateTimeToTime(s["started_at"])} content={timeInfo} placement="right"/>
+ </td>
<td>{durationBetween(s["started_at"], s["completed_at"])}</td>
<td>{formatSize(s["size"])}</td>
<td>{s["processed_packets"]}</td>
@@ -121,8 +157,8 @@ class PcapPane extends Component {
placement="left"/></td>
<td className="table-cell-action"><a href={"/api/pcap/sessions/" + s["id"] + "/download"}>download</a>
</td>
- </tr>
- );
+ </tr>;
+ });
const handleUploadFileChange = (file) => {
this.setState({
@@ -144,16 +180,16 @@ class PcapPane extends Component {
});
};
- const uploadCurlCommand = createCurlCommand("pcap/upload", "POST", null, {
- file: "@" + ((this.state.uploadSelectedFile != null && this.state.isUploadFileValid) ?
+ const uploadCurlCommand = createCurlCommand("/pcap/upload", "POST", null, {
+ "file": "@" + ((this.state.uploadSelectedFile != null && this.state.isUploadFileValid) ?
this.state.uploadSelectedFile.name : "invalid.pcap"),
- flush_all: this.state.uploadFlushAll
+ "flush_all": this.state.uploadFlushAll
});
- const fileCurlCommand = createCurlCommand("pcap/file", "POST", {
- file: this.state.fileValue,
- flush_all: this.state.processFlushAll,
- delete_original_file: this.state.deleteOriginalFile
+ const fileCurlCommand = createCurlCommand("/pcap/file", "POST", {
+ "file": this.state.fileValue,
+ "flush_all": this.state.processFlushAll,
+ "delete_original_file": this.state.deleteOriginalFile
});
return (
@@ -207,7 +243,7 @@ class PcapPane extends Component {
<div className="upload-options">
<span>options:</span>
<CheckField name="flush_all" checked={this.state.uploadFlushAll}
- onChange={v => this.setState({uploadFlushAll: v})}/>
+ onChange={(v) => this.setState({uploadFlushAll: v})}/>
</div>
<ButtonField variant="green" bordered onClick={this.uploadPcap} name="upload"/>
</div>
@@ -232,9 +268,9 @@ class PcapPane extends Component {
<div className="upload-actions" style={{"marginTop": "11px"}}>
<div className="upload-options">
<CheckField name="flush_all" checked={this.state.processFlushAll}
- onChange={v => this.setState({processFlushAll: v})}/>
+ onChange={(v) => this.setState({processFlushAll: v})}/>
<CheckField name="delete_original_file" checked={this.state.deleteOriginalFile}
- onChange={v => this.setState({deleteOriginalFile: v})}/>
+ onChange={(v) => this.setState({deleteOriginalFile: v})}/>
</div>
<ButtonField variant="blue" bordered onClick={this.processPcap} name="process"/>
</div>
@@ -248,4 +284,4 @@ class PcapPane extends Component {
}
}
-export default PcapPane;
+export default PcapsPane;
diff --git a/frontend/src/components/panels/PcapPane.scss b/frontend/src/components/panels/PcapsPane.scss
index 4dbc2b2..4dbc2b2 100644
--- a/frontend/src/components/panels/PcapPane.scss
+++ b/frontend/src/components/panels/PcapsPane.scss
diff --git a/frontend/src/components/panels/RulePane.js b/frontend/src/components/panels/RulesPane.js
index 49364d2..cdfe185 100644
--- a/frontend/src/components/panels/RulePane.js
+++ b/frontend/src/components/panels/RulesPane.js
@@ -1,45 +1,42 @@
-import React, {Component} from 'react';
-import './common.scss';
-import './RulePane.scss';
-import Table from "react-bootstrap/Table";
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import React, {Component} from "react";
import {Col, Container, Row} from "react-bootstrap";
-import InputField from "../fields/InputField";
-import CheckField from "../fields/CheckField";
-import TextField from "../fields/TextField";
+import Table from "react-bootstrap/Table";
import backend from "../../backend";
-import NumericField from "../fields/extensions/NumericField";
-import ColorField from "../fields/extensions/ColorField";
-import ChoiceField from "../fields/ChoiceField";
-import ButtonField from "../fields/ButtonField";
+import dispatcher from "../../dispatcher";
import validation from "../../validation";
+import ButtonField from "../fields/ButtonField";
+import CheckField from "../fields/CheckField";
+import ChoiceField from "../fields/ChoiceField";
+import ColorField from "../fields/extensions/ColorField";
+import NumericField from "../fields/extensions/NumericField";
+import InputField from "../fields/InputField";
+import TextField from "../fields/TextField";
+import CopyLinkPopover from "../objects/CopyLinkPopover";
import LinkPopover from "../objects/LinkPopover";
-import {randomClassName} from "../../utils";
-
-const classNames = require('classnames');
-const _ = require('lodash');
-
-class RulePane extends Component {
-
- constructor(props) {
- super(props);
-
- this.state = {
- rules: [],
- newRule: this.emptyRule,
- newPattern: this.emptyPattern
- };
+import "./common.scss";
+import "./RulesPane.scss";
- this.directions = {
- 0: "both",
- 1: "c->s",
- 2: "s->c"
- };
- }
+const classNames = require("classnames");
+const _ = require("lodash");
- componentDidMount() {
- this.reset();
- this.loadRules();
- }
+class RulesPane extends Component {
emptyRule = {
"name": "",
@@ -58,7 +55,6 @@ class RulePane extends Component {
},
"version": 0
};
-
emptyPattern = {
"regex": "",
"flags": {
@@ -72,19 +68,52 @@ class RulePane extends Component {
"max_occurrences": 0,
"direction": 0
};
+ state = {
+ rules: [],
+ newRule: this.emptyRule,
+ newPattern: this.emptyPattern
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.directions = {
+ 0: "both",
+ 1: "c->s",
+ 2: "s->c"
+ };
+ }
+
+ componentDidMount() {
+ this.reset();
+ this.loadRules();
+
+ dispatcher.register("notifications", this.handleNotifications);
+ document.title = "caronte:~/rules$";
+ }
+
+ componentWillUnmount() {
+ dispatcher.unregister(this.handleNotifications);
+ }
+
+ handleNotifications = (payload) => {
+ if (payload.event === "rules.new" || payload.event === "rules.edit") {
+ this.loadRules();
+ }
+ };
loadRules = () => {
- backend.get("/api/rules").then(res => this.setState({rules: res.json, rulesStatusCode: res.status}))
- .catch(res => this.setState({rulesStatusCode: res.status, rulesResponse: JSON.stringify(res.json)}));
+ backend.get("/api/rules").then((res) => this.setState({rules: res.json, rulesStatusCode: res.status}))
+ .catch((res) => this.setState({rulesStatusCode: res.status, rulesResponse: JSON.stringify(res.json)}));
};
addRule = () => {
if (this.validateRule(this.state.newRule)) {
- backend.post("/api/rules", this.state.newRule).then(res => {
+ backend.post("/api/rules", this.state.newRule).then((res) => {
this.reset();
this.setState({ruleStatusCode: res.status});
this.loadRules();
- }).catch(res => {
+ }).catch((res) => {
this.setState({ruleStatusCode: res.status, ruleResponse: JSON.stringify(res.json)});
});
}
@@ -93,11 +122,11 @@ class RulePane extends Component {
updateRule = () => {
const rule = this.state.selectedRule;
if (this.validateRule(rule)) {
- backend.put(`/api/rules/${rule.id}`, rule).then(res => {
+ backend.put(`/api/rules/${rule.id}`, rule).then((res) => {
this.reset();
this.setState({ruleStatusCode: res.status});
this.loadRules();
- }).catch(res => {
+ }).catch((res) => {
this.setState({ruleStatusCode: res.status, ruleResponse: JSON.stringify(res.json)});
});
}
@@ -113,23 +142,23 @@ class RulePane extends Component {
this.setState({ruleColorError: "color is not hexcolor"});
valid = false;
}
- if (!validation.isValidPort(rule.filter.service_port)) {
+ if (!validation.isValidPort(rule.filter["service_port"])) {
this.setState({ruleServicePortError: "service_port > 65565"});
valid = false;
}
- if (!validation.isValidPort(rule.filter.client_port)) {
+ if (!validation.isValidPort(rule.filter["client_port"])) {
this.setState({ruleClientPortError: "client_port > 65565"});
valid = false;
}
- if (!validation.isValidAddress(rule.filter.client_address)) {
+ if (!validation.isValidAddress(rule.filter["client_address"])) {
this.setState({ruleClientAddressError: "client_address is not ip_address"});
valid = false;
}
- if (rule.filter.min_duration > rule.filter.max_duration) {
+ if (rule.filter["min_duration"] > rule.filter["max_duration"]) {
this.setState({ruleDurationError: "min_duration > max_dur."});
valid = false;
}
- if (rule.filter.min_bytes > rule.filter.max_bytes) {
+ if (rule.filter["min_bytes"] > rule.filter["max_bytes"]) {
this.setState({ruleBytesError: "min_bytes > max_bytes"});
valid = false;
}
@@ -146,9 +175,9 @@ class RulePane extends Component {
const newPattern = _.cloneDeep(this.emptyPattern);
this.setState({
selectedRule: null,
- newRule: newRule,
+ newRule,
selectedPattern: null,
- newPattern: newPattern,
+ newPattern,
patternRegexFocused: false,
patternOccurrencesFocused: false,
ruleNameError: null,
@@ -181,9 +210,7 @@ class RulePane extends Component {
const newPattern = _.cloneDeep(this.emptyPattern);
this.currentRule().patterns.push(pattern);
- this.setState({
- newPattern: newPattern
- });
+ this.setState({newPattern});
};
editPattern = (pattern) => {
@@ -208,7 +235,7 @@ class RulePane extends Component {
valid = false;
this.setState({patternRegexFocused: true});
}
- if (pattern.min_occurrences > pattern.max_occurrences) {
+ if (pattern["min_occurrences"] > pattern["max_occurrences"]) {
valid = false;
this.setState({patternOccurrencesFocused: true});
}
@@ -220,71 +247,72 @@ class RulePane extends Component {
const rule = this.currentRule();
const pattern = this.state.selectedPattern || this.state.newPattern;
- let rules = this.state.rules.map(r =>
+ let rules = this.state.rules.map((r) =>
<tr key={r.id} onClick={() => {
this.reset();
this.setState({selectedRule: _.cloneDeep(r)});
- }} className={classNames("row-small", "row-clickable", {"row-selected": rule.id === r.id })}>
- <td>{r["id"].substring(0, 8)}</td>
+ }} className={classNames("row-small", "row-clickable", {"row-selected": rule.id === r.id})}>
+ <CopyLinkPopover text={r["id"].substring(0, 8)} value={r["id"]}/>
<td>{r["name"]}</td>
- <td><ButtonField name={r["color"]} color={r["color"]} small /></td>
+ <td><ButtonField name={r["color"]} color={r["color"]} small/></td>
<td>{r["notes"]}</td>
</tr>
);
let patterns = (this.state.selectedPattern == null && !isUpdate ?
- rule.patterns.concat(this.state.newPattern) :
- rule.patterns
- ).map(p => p === pattern ?
- <tr key={randomClassName()}>
+ rule.patterns.concat(this.state.newPattern) :
+ rule.patterns
+ ).map((p) => p === pattern ?
+ <tr key={"new_pattern"}>
<td style={{"width": "500px"}}>
<InputField small active={this.state.patternRegexFocused} value={pattern.regex}
onChange={(v) => {
this.updateParam(() => pattern.regex = v);
this.setState({patternRegexFocused: pattern.regex === ""});
- }} />
+ }}/>
</td>
- <td><CheckField small checked={pattern.flags.caseless}
- onChange={(v) => this.updateParam(() => pattern.flags.caseless = v)}/></td>
- <td><CheckField small checked={pattern.flags.dot_all}
- onChange={(v) => this.updateParam(() => pattern.flags.dot_all = v)}/></td>
- <td><CheckField small checked={pattern.flags.multi_line}
- onChange={(v) => this.updateParam(() => pattern.flags.multi_line = v)}/></td>
- <td><CheckField small checked={pattern.flags.utf_8_mode}
- onChange={(v) => this.updateParam(() => pattern.flags.utf_8_mode = v)}/></td>
- <td><CheckField small checked={pattern.flags.unicode_property}
- onChange={(v) => this.updateParam(() => pattern.flags.unicode_property = v)}/></td>
+ <td><CheckField small checked={pattern.flags["caseless"]}
+ onChange={(v) => this.updateParam(() => pattern.flags["caseless"] = v)}/></td>
+ <td><CheckField small checked={pattern.flags["dot_all"]}
+ onChange={(v) => this.updateParam(() => pattern.flags["dot_all"] = v)}/></td>
+ <td><CheckField small checked={pattern.flags["multi_line"]}
+ onChange={(v) => this.updateParam(() => pattern.flags["multi_line"] = v)}/></td>
+ <td><CheckField small checked={pattern.flags["utf_8_mode"]}
+ onChange={(v) => this.updateParam(() => pattern.flags["utf_8_mode"] = v)}/></td>
+ <td><CheckField small checked={pattern.flags["unicode_property"]}
+ onChange={(v) => this.updateParam(() => pattern.flags["unicode_property"] = v)}/></td>
<td style={{"width": "70px"}}>
- <NumericField small value={pattern.min_occurrences}
+ <NumericField small value={pattern["min_occurrences"]}
active={this.state.patternOccurrencesFocused}
- onChange={(v) => this.updateParam(() => pattern.min_occurrences = v)} />
+ onChange={(v) => this.updateParam(() => pattern["min_occurrences"] = v)}/>
</td>
<td style={{"width": "70px"}}>
- <NumericField small value={pattern.max_occurrences}
+ <NumericField small value={pattern["max_occurrences"]}
active={this.state.patternOccurrencesFocused}
- onChange={(v) => this.updateParam(() => pattern.max_occurrences = v)} />
+ onChange={(v) => this.updateParam(() => pattern["max_occurrences"] = v)}/>
</td>
<td><ChoiceField inline small keys={[0, 1, 2]} values={["both", "c->s", "s->c"]}
value={this.directions[pattern.direction]}
- onChange={(v) => this.updateParam(() => pattern.direction = v)} /></td>
+ onChange={(v) => this.updateParam(() => pattern.direction = v)}/></td>
<td>{this.state.selectedPattern == null ?
<ButtonField variant="green" small name="add" inline rounded onClick={() => this.addPattern(p)}/> :
- <ButtonField variant="green" small name="save" inline rounded onClick={() => this.updatePattern(p)}/>}
+ <ButtonField variant="green" small name="save" inline rounded
+ onClick={() => this.updatePattern(p)}/>}
</td>
</tr>
:
<tr key={"new_pattern"} className="row-small">
<td>{p.regex}</td>
- <td>{p.flags.caseless ? "yes": "no"}</td>
- <td>{p.flags.dot_all ? "yes": "no"}</td>
- <td>{p.flags.multi_line ? "yes": "no"}</td>
- <td>{p.flags.utf_8_mode ? "yes": "no"}</td>
- <td>{p.flags.unicode_property ? "yes": "no"}</td>
- <td>{p.min_occurrences}</td>
- <td>{p.max_occurrences}</td>
+ <td>{p.flags["caseless"] ? "yes" : "no"}</td>
+ <td>{p.flags["dot_all"] ? "yes" : "no"}</td>
+ <td>{p.flags["multi_line"] ? "yes" : "no"}</td>
+ <td>{p.flags["utf_8_mode"] ? "yes" : "no"}</td>
+ <td>{p.flags["unicode_property"] ? "yes" : "no"}</td>
+ <td>{p["min_occurrences"]}</td>
+ <td>{p["max_occurrences"]}</td>
<td>{this.directions[p.direction]}</td>
{!isUpdate && <td><ButtonField variant="blue" small rounded name="edit"
- onClick={() => this.editPattern(p) }/></td>}
+ onClick={() => this.editPattern(p)}/></td>}
</tr>
);
@@ -294,9 +322,9 @@ class RulePane extends Component {
<div className="section-header">
<span className="api-request">GET /api/rules</span>
{this.state.rulesStatusCode &&
- <span className="api-response"><LinkPopover text={this.state.rulesStatusCode}
- content={this.state.rulesResponse}
- placement="left" /></span>}
+ <span className="api-response"><LinkPopover text={this.state.rulesStatusCode}
+ content={this.state.rulesResponse}
+ placement="left"/></span>}
</div>
<div className="section-content">
@@ -325,7 +353,7 @@ class RulePane extends Component {
</span>
<span className="api-response"><LinkPopover text={this.state.ruleStatusCode}
content={this.state.ruleResponse}
- placement="left" /></span>
+ placement="left"/></span>
</div>
<div className="section-content">
@@ -334,41 +362,41 @@ class RulePane extends Component {
<Col>
<InputField name="name" inline value={rule.name}
onChange={(v) => this.updateParam((r) => r.name = v)}
- error={this.state.ruleNameError} />
+ error={this.state.ruleNameError}/>
<ColorField inline value={rule.color} error={this.state.ruleColorError}
- onChange={(v) => this.updateParam((r) => r.color = v)} />
+ onChange={(v) => this.updateParam((r) => r.color = v)}/>
<TextField name="notes" rows={2} value={rule.notes}
- onChange={(v) => this.updateParam((r) => r.notes = v)} />
+ onChange={(v) => this.updateParam((r) => r.notes = v)}/>
</Col>
<Col style={{"paddingTop": "6px"}}>
<span>filters:</span>
- <NumericField name="service_port" inline value={rule.filter.service_port}
- onChange={(v) => this.updateParam((r) => r.filter.service_port = v)}
+ <NumericField name="service_port" inline value={rule.filter["service_port"]}
+ onChange={(v) => this.updateParam((r) => r.filter["service_port"] = v)}
min={0} max={65565} error={this.state.ruleServicePortError}
- readonly={isUpdate} />
- <NumericField name="client_port" inline value={rule.filter.client_port}
- onChange={(v) => this.updateParam((r) => r.filter.client_port = v)}
+ readonly={isUpdate}/>
+ <NumericField name="client_port" inline value={rule.filter["client_port"]}
+ onChange={(v) => this.updateParam((r) => r.filter["client_port"] = v)}
min={0} max={65565} error={this.state.ruleClientPortError}
- readonly={isUpdate} />
- <InputField name="client_address" value={rule.filter.client_address}
+ readonly={isUpdate}/>
+ <InputField name="client_address" value={rule.filter["client_address"]}
error={this.state.ruleClientAddressError} readonly={isUpdate}
- onChange={(v) => this.updateParam((r) => r.filter.client_address = v)} />
+ onChange={(v) => this.updateParam((r) => r.filter["client_address"] = v)}/>
</Col>
<Col style={{"paddingTop": "11px"}}>
- <NumericField name="min_duration" inline value={rule.filter.min_duration}
+ <NumericField name="min_duration" inline value={rule.filter["min_duration"]}
error={this.state.ruleDurationError} readonly={isUpdate}
- onChange={(v) => this.updateParam((r) => r.filter.min_duration = v)} />
- <NumericField name="max_duration" inline value={rule.filter.max_duration}
+ onChange={(v) => this.updateParam((r) => r.filter["min_duration"] = v)}/>
+ <NumericField name="max_duration" inline value={rule.filter["max_duration"]}
error={this.state.ruleDurationError} readonly={isUpdate}
- onChange={(v) => this.updateParam((r) => r.filter.max_duration = v)} />
- <NumericField name="min_bytes" inline value={rule.filter.min_bytes}
+ onChange={(v) => this.updateParam((r) => r.filter["max_duration"] = v)}/>
+ <NumericField name="min_bytes" inline value={rule.filter["min_bytes"]}
error={this.state.ruleBytesError} readonly={isUpdate}
- onChange={(v) => this.updateParam((r) => r.filter.min_bytes = v)} />
- <NumericField name="max_bytes" inline value={rule.filter.max_bytes}
+ onChange={(v) => this.updateParam((r) => r.filter["min_bytes"] = v)}/>
+ <NumericField name="max_bytes" inline value={rule.filter["max_bytes"]}
error={this.state.ruleBytesError} readonly={isUpdate}
- onChange={(v) => this.updateParam((r) => r.filter.max_bytes = v)} />
+ onChange={(v) => this.updateParam((r) => r.filter["max_bytes"] = v)}/>
</Col>
</Row>
</Container>
@@ -386,7 +414,7 @@ class RulePane extends Component {
<th>min</th>
<th>max</th>
<th>direction</th>
- {!isUpdate && <th>actions</th> }
+ {!isUpdate && <th>actions</th>}
</tr>
</thead>
<tbody>
@@ -401,7 +429,7 @@ class RulePane extends Component {
<div className="section-footer">
{<ButtonField variant="red" name="cancel" bordered onClick={this.reset}/>}
<ButtonField variant={isUpdate ? "blue" : "green"} name={isUpdate ? "update_rule" : "add_rule"}
- bordered onClick={isUpdate ? this.updateRule : this.addRule} />
+ bordered onClick={isUpdate ? this.updateRule : this.addRule}/>
</div>
</div>
</div>
@@ -410,4 +438,4 @@ class RulePane extends Component {
}
-export default RulePane;
+export default RulesPane;
diff --git a/frontend/src/components/panels/RulePane.scss b/frontend/src/components/panels/RulesPane.scss
index 992445a..992445a 100644
--- a/frontend/src/components/panels/RulePane.scss
+++ b/frontend/src/components/panels/RulesPane.scss
diff --git a/frontend/src/components/panels/SearchPane.js b/frontend/src/components/panels/SearchPane.js
new file mode 100644
index 0000000..4ef5632
--- /dev/null
+++ b/frontend/src/components/panels/SearchPane.js
@@ -0,0 +1,309 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import React, {Component} from "react";
+import Table from "react-bootstrap/Table";
+import backend from "../../backend";
+import dispatcher from "../../dispatcher";
+import {createCurlCommand, dateTimeToTime, durationBetween} from "../../utils";
+import ButtonField from "../fields/ButtonField";
+import CheckField from "../fields/CheckField";
+import InputField from "../fields/InputField";
+import TagField from "../fields/TagField";
+import TextField from "../fields/TextField";
+import LinkPopover from "../objects/LinkPopover";
+import "./common.scss";
+import "./SearchPane.scss";
+
+const _ = require("lodash");
+
+class SearchPane extends Component {
+
+ searchOptions = {
+ "text_search": {
+ "terms": null,
+ "excluded_terms": null,
+ "exact_phrase": "",
+ "case_sensitive": false
+ },
+ "regex_search": {
+ "pattern": "",
+ "not_pattern": "",
+ "case_insensitive": false,
+ "multi_line": false,
+ "ignore_whitespaces": false,
+ "dot_character": false
+ },
+ "timeout": 10
+ };
+
+ state = {
+ searches: [],
+ currentSearchOptions: this.searchOptions,
+ };
+
+ componentDidMount() {
+ this.reset();
+ this.loadSearches();
+
+ dispatcher.register("notifications", this.handleNotification);
+ document.title = "caronte:~/searches$";
+ }
+
+ componentWillUnmount() {
+ dispatcher.unregister(this.handleNotification);
+ }
+
+ loadSearches = () => {
+ backend.get("/api/searches")
+ .then((res) => this.setState({searches: res.json, searchesStatusCode: res.status}))
+ .catch((res) => this.setState({searchesStatusCode: res.status, searchesResponse: JSON.stringify(res.json)}));
+ };
+
+ performSearch = () => {
+ const options = this.state.currentSearchOptions;
+ this.setState({loading: true});
+ if (this.validateSearch(options)) {
+ backend.post("/api/searches/perform", options).then((res) => {
+ this.reset();
+ this.setState({searchStatusCode: res.status, loading: false});
+ this.loadSearches();
+ this.viewSearch(res.json.id);
+ }).catch((res) => {
+ this.setState({
+ searchStatusCode: res.status, searchResponse: JSON.stringify(res.json),
+ loading: false
+ });
+ });
+ }
+ };
+
+ reset = () => {
+ this.setState({
+ currentSearchOptions: _.cloneDeep(this.searchOptions),
+ exactPhraseError: null,
+ patternError: null,
+ notPatternError: null,
+ searchStatusCode: null,
+ searchesStatusCode: null,
+ searchResponse: null,
+ searchesResponse: null
+ });
+ };
+
+ validateSearch = (options) => {
+ let valid = true;
+ if (options["text_search"]["exact_phrase"] && options["text_search"]["exact_phrase"].length < 3) {
+ this.setState({exactPhraseError: "text_search.exact_phrase.length < 3"});
+ valid = false;
+ }
+ if (options["regex_search"].pattern && options["regex_search"].pattern.length < 3) {
+ this.setState({patternError: "regex_search.pattern.length < 3"});
+ valid = false;
+ }
+ if (options["regex_search"]["not_pattern"] && options["regex_search"]["not_pattern"].length < 3) {
+ this.setState({exactPhraseError: "regex_search.not_pattern.length < 3"});
+ valid = false;
+ }
+
+ return valid;
+ };
+
+ updateParam = (callback) => {
+ callback(this.state.currentSearchOptions);
+ this.setState({currentSearchOptions: this.state.currentSearchOptions});
+ };
+
+ extractPattern = (options) => {
+ let pattern = "";
+ if (_.isEqual(options.regex_search, this.searchOptions.regex_search)) { // is text search
+ if (options["text_search"]["exact_phrase"]) {
+ pattern += `"${options["text_search"]["exact_phrase"]}"`;
+ } else {
+ pattern += options["text_search"].terms.join(" ");
+ if (options["text_search"]["excluded_terms"]) {
+ pattern += " -" + options["text_search"]["excluded_terms"].join(" -");
+ }
+ }
+ options["text_search"]["case_sensitive"] && (pattern += "/s");
+ } else { // is regex search
+ if (options["regex_search"].pattern) {
+ pattern += "/" + options["regex_search"].pattern + "/";
+ } else {
+ pattern += "!/" + options["regex_search"]["not_pattern"] + "/";
+ }
+ options["regex_search"]["case_insensitive"] && (pattern += "i");
+ options["regex_search"]["multi_line"] && (pattern += "m");
+ options["regex_search"]["ignore_whitespaces"] && (pattern += "x");
+ options["regex_search"]["dot_character"] && (pattern += "s");
+ }
+
+ return pattern;
+ };
+
+ viewSearch = (searchId) => {
+ dispatcher.dispatch("connections_filters", {"performed_search": searchId});
+ };
+
+ handleNotification = (payload) => {
+ if (payload.event === "searches.new") {
+ this.loadSearches();
+ }
+ };
+
+ render() {
+ const options = this.state.currentSearchOptions;
+
+ let searches = this.state.searches.map((s) =>
+ <tr key={s.id} className="row-small row-clickable">
+ <td>{s.id.substring(0, 8)}</td>
+ <td>{this.extractPattern(s["search_options"])}</td>
+ <td>{s["affected_connections_count"]}</td>
+ <td>{dateTimeToTime(s["started_at"])}</td>
+ <td>{durationBetween(s["started_at"], s["finished_at"])}</td>
+ <td><ButtonField name="view" variant="green" small onClick={() => this.viewSearch(s.id)}/></td>
+ </tr>
+ );
+
+ const textOptionsModified = !_.isEqual(this.searchOptions.text_search, options.text_search);
+ const regexOptionsModified = !_.isEqual(this.searchOptions.regex_search, options.regex_search);
+
+ const curlCommand = createCurlCommand("/searches/perform", "POST", options);
+
+ return (
+ <div className="pane-container search-pane">
+ <div className="pane-section searches-list">
+ <div className="section-header">
+ <span className="api-request">GET /api/searches</span>
+ {this.state.searchesStatusCode &&
+ <span className="api-response"><LinkPopover text={this.state.searchesStatusCode}
+ content={this.state.searchesResponse}
+ placement="left"/></span>}
+ </div>
+
+ <div className="section-content">
+ <div className="section-table">
+ <Table borderless size="sm">
+ <thead>
+ <tr>
+ <th>id</th>
+ <th>pattern</th>
+ <th>occurrences</th>
+ <th>started_at</th>
+ <th>duration</th>
+ <th>actions</th>
+ </tr>
+ </thead>
+ <tbody>
+ {searches}
+ </tbody>
+ </Table>
+ </div>
+ </div>
+ </div>
+
+ <div className="pane-section search-new">
+ <div className="section-header">
+ <span className="api-request">POST /api/searches/perform</span>
+ <span className="api-response"><LinkPopover text={this.state.searchStatusCode}
+ content={this.state.searchResponse}
+ placement="left"/></span>
+ </div>
+
+ <div className="section-content">
+ <span className="notes">
+ NOTE: it is recommended to use the rules for recurring themes. Give preference to textual search over that with regex.
+ </span>
+
+ <div className="content-row">
+ <div className="text-search">
+ <TagField tags={(options["text_search"].terms || []).map((t) => {
+ return {name: t};
+ })}
+ name="terms" min={3} inline allowNew={true}
+ readonly={regexOptionsModified || options["text_search"]["exact_phrase"]}
+ onChange={(tags) => this.updateParam((s) => s["text_search"].terms = tags.map((t) => t.name))}/>
+ <TagField tags={(options["text_search"]["excluded_terms"] || []).map((t) => {
+ return {name: t};
+ })}
+ name="excluded_terms" min={3} inline allowNew={true}
+ readonly={regexOptionsModified || options["text_search"]["exact_phrase"]}
+ onChange={(tags) => this.updateParam((s) => s["text_search"]["excluded_terms"] = tags.map((t) => t.name))}/>
+
+ <span className="exclusive-separator">or</span>
+
+ <InputField name="exact_phrase" value={options["text_search"]["exact_phrase"]} inline
+ error={this.state.exactPhraseError}
+ onChange={(v) => this.updateParam((s) => s["text_search"]["exact_phrase"] = v)}
+ readonly={regexOptionsModified || (Array.isArray(options["text_search"].terms) && options["text_search"].terms.length > 0)}/>
+
+ <CheckField checked={options["text_search"]["case_sensitive"]} name="case_sensitive"
+ readonly={regexOptionsModified} small
+ onChange={(v) => this.updateParam((s) => s["text_search"]["case_sensitive"] = v)}/>
+ </div>
+
+ <div className="separator">
+ <span>or</span>
+ </div>
+
+ <div className="regex-search">
+ <InputField name="pattern" value={options["regex_search"].pattern} inline
+ error={this.state.patternError}
+ readonly={textOptionsModified || options["regex_search"]["not_pattern"]}
+ onChange={(v) => this.updateParam((s) => s["regex_search"].pattern = v)}/>
+ <span className="exclusive-separator">or</span>
+ <InputField name="not_pattern" value={options["regex_search"]["not_pattern"]} inline
+ error={this.state.notPatternError}
+ readonly={textOptionsModified || options["regex_search"].pattern}
+ onChange={(v) => this.updateParam((s) => s["regex_search"]["not_pattern"] = v)}/>
+
+ <div className="checkbox-line">
+ <CheckField checked={options["regex_search"]["case_insensitive"]}
+ name="case_insensitive"
+ readonly={textOptionsModified} small
+ onChange={(v) => this.updateParam((s) => s["regex_search"]["case_insensitive"] = v)}/>
+ <CheckField checked={options["regex_search"]["multi_line"]} name="multi_line"
+ readonly={textOptionsModified} small
+ onChange={(v) => this.updateParam((s) => s["regex_search"]["multi_line"] = v)}/>
+ <CheckField checked={options["regex_search"]["ignore_whitespaces"]}
+ name="ignore_whitespaces"
+ readonly={textOptionsModified} small
+ onChange={(v) => this.updateParam((s) => s["regex_search"]["ignore_whitespaces"] = v)}/>
+ <CheckField checked={options["regex_search"]["dot_character"]} name="dot_character"
+ readonly={textOptionsModified} small
+ onChange={(v) => this.updateParam((s) => s["regex_search"]["dot_character"] = v)}/>
+ </div>
+ </div>
+ </div>
+
+ <TextField value={curlCommand} rows={3} readonly small={true}/>
+ </div>
+
+ <div className="section-footer">
+ <ButtonField variant="red" name="cancel" bordered disabled={this.state.loading}
+ onClick={this.reset}/>
+ <ButtonField variant="green" name="perform_search" bordered
+ disabled={this.state.loading} onClick={this.performSearch}/>
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+}
+
+export default SearchPane;
diff --git a/frontend/src/components/panels/SearchPane.scss b/frontend/src/components/panels/SearchPane.scss
new file mode 100644
index 0000000..63e11fb
--- /dev/null
+++ b/frontend/src/components/panels/SearchPane.scss
@@ -0,0 +1,52 @@
+.search-pane {
+ display: flex;
+ flex-direction: column;
+
+ .searches-list {
+ overflow: hidden;
+ flex: 2 1;
+
+ .section-content {
+ height: 100%;
+ }
+
+ .section-table {
+ height: calc(100% - 30px);
+ }
+ }
+
+ .search-new {
+ .content-row {
+ display: flex;
+
+ .text-search,
+ .regex-search {
+ flex: 1;
+ }
+
+ .exclusive-separator {
+ font-size: 0.8em;
+ display: block;
+ text-align: center;
+ }
+
+ .separator {
+ font-size: 0.9em;
+ flex: 0;
+ margin: auto 10px;
+ }
+ }
+
+ .notes {
+ font-size: 0.8em;
+ }
+
+ .checkbox-line {
+ .check-field {
+ display: inline-block;
+ margin-top: 0;
+ margin-right: 10px;
+ }
+ }
+ }
+}
diff --git a/frontend/src/components/panels/ServicePane.js b/frontend/src/components/panels/ServicesPane.js
index eaefa64..5986804 100644
--- a/frontend/src/components/panels/ServicePane.js
+++ b/frontend/src/components/panels/ServicesPane.js
@@ -1,58 +1,85 @@
-import React, {Component} from 'react';
-import './common.scss';
-import './ServicePane.scss';
-import Table from "react-bootstrap/Table";
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import React, {Component} from "react";
import {Col, Container, Row} from "react-bootstrap";
-import InputField from "../fields/InputField";
-import TextField from "../fields/TextField";
+import Table from "react-bootstrap/Table";
import backend from "../../backend";
-import NumericField from "../fields/extensions/NumericField";
-import ColorField from "../fields/extensions/ColorField";
-import ButtonField from "../fields/ButtonField";
+import dispatcher from "../../dispatcher";
+import {createCurlCommand} from "../../utils";
import validation from "../../validation";
+import ButtonField from "../fields/ButtonField";
+import ColorField from "../fields/extensions/ColorField";
+import NumericField from "../fields/extensions/NumericField";
+import InputField from "../fields/InputField";
+import TextField from "../fields/TextField";
import LinkPopover from "../objects/LinkPopover";
-import {createCurlCommand} from "../../utils";
+import "./common.scss";
+import "./ServicesPane.scss";
-const classNames = require('classnames');
-const _ = require('lodash');
+const classNames = require("classnames");
+const _ = require("lodash");
-class ServicePane extends Component {
+class ServicesPane extends Component {
- constructor(props) {
- super(props);
+ emptyService = {
+ "port": 0,
+ "name": "",
+ "color": "",
+ "notes": ""
+ };
- this.state = {
- services: [],
- currentService: this.emptyService,
- };
- }
+ state = {
+ services: [],
+ currentService: this.emptyService,
+ };
componentDidMount() {
this.reset();
this.loadServices();
+
+ dispatcher.register("notifications", this.handleNotifications);
+ document.title = "caronte:~/services$";
}
- emptyService = {
- "port": 0,
- "name": "",
- "color": "",
- "notes": ""
+ componentWillUnmount() {
+ dispatcher.unregister(this.handleNotifications);
+ }
+
+ handleNotifications = (payload) => {
+ if (payload.event === "services.edit") {
+ this.loadServices();
+ }
};
loadServices = () => {
backend.get("/api/services")
- .then(res => this.setState({services: Object.values(res.json), servicesStatusCode: res.status}))
- .catch(res => this.setState({servicesStatusCode: res.status, servicesResponse: JSON.stringify(res.json)}));
+ .then((res) => this.setState({services: Object.values(res.json), servicesStatusCode: res.status}))
+ .catch((res) => this.setState({servicesStatusCode: res.status, servicesResponse: JSON.stringify(res.json)}));
};
updateService = () => {
const service = this.state.currentService;
if (this.validateService(service)) {
- backend.put("/api/services", service).then(res => {
+ backend.put("/api/services", service).then((res) => {
this.reset();
this.setState({serviceStatusCode: res.status});
this.loadServices();
- }).catch(res => {
+ }).catch((res) => {
this.setState({serviceStatusCode: res.status, serviceResponse: JSON.stringify(res.json)});
});
}
@@ -99,14 +126,14 @@ class ServicePane extends Component {
const isUpdate = this.state.isUpdate;
const service = this.state.currentService;
- let services = this.state.services.map(s =>
+ let services = this.state.services.map((s) =>
<tr key={s.port} onClick={() => {
this.reset();
this.setState({isUpdate: true, currentService: _.cloneDeep(s)});
- }} className={classNames("row-small", "row-clickable", {"row-selected": service.port === s.port })}>
+ }} className={classNames("row-small", "row-clickable", {"row-selected": service.port === s.port})}>
<td>{s["port"]}</td>
<td>{s["name"]}</td>
- <td><ButtonField name={s["color"]} color={s["color"]} small /></td>
+ <td><ButtonField name={s["color"]} color={s["color"]} small/></td>
<td>{s["notes"]}</td>
</tr>
);
@@ -119,9 +146,9 @@ class ServicePane extends Component {
<div className="section-header">
<span className="api-request">GET /api/services</span>
{this.state.servicesStatusCode &&
- <span className="api-response"><LinkPopover text={this.state.servicesStatusCode}
- content={this.state.servicesResponse}
- placement="left" /></span>}
+ <span className="api-response"><LinkPopover text={this.state.servicesStatusCode}
+ content={this.state.servicesResponse}
+ placement="left"/></span>}
</div>
<div className="section-content">
@@ -148,7 +175,7 @@ class ServicePane extends Component {
<span className="api-request">PUT /api/services</span>
<span className="api-response"><LinkPopover text={this.state.serviceStatusCode}
content={this.state.serviceResponse}
- placement="left" /></span>
+ placement="left"/></span>
</div>
<div className="section-content">
@@ -157,17 +184,17 @@ class ServicePane extends Component {
<Col>
<NumericField name="port" value={service.port}
onChange={(v) => this.updateParam((s) => s.port = v)}
- min={0} max={65565} error={this.state.servicePortError} />
+ min={0} max={65565} error={this.state.servicePortError}/>
<InputField name="name" value={service.name}
onChange={(v) => this.updateParam((s) => s.name = v)}
- error={this.state.serviceNameError} />
+ error={this.state.serviceNameError}/>
<ColorField value={service.color} error={this.state.serviceColorError}
- onChange={(v) => this.updateParam((s) => s.color = v)} />
+ onChange={(v) => this.updateParam((s) => s.color = v)}/>
</Col>
<Col>
<TextField name="notes" rows={7} value={service.notes}
- onChange={(v) => this.updateParam((s) => s.notes = v)} />
+ onChange={(v) => this.updateParam((s) => s.notes = v)}/>
</Col>
</Row>
</Container>
@@ -177,8 +204,9 @@ class ServicePane extends Component {
<div className="section-footer">
{<ButtonField variant="red" name="cancel" bordered onClick={this.reset}/>}
- <ButtonField variant={isUpdate ? "blue" : "green"} name={isUpdate ? "update_service" : "add_service"}
- bordered onClick={this.updateService} />
+ <ButtonField variant={isUpdate ? "blue" : "green"}
+ name={isUpdate ? "update_service" : "add_service"}
+ bordered onClick={this.updateService}/>
</div>
</div>
</div>
@@ -187,4 +215,4 @@ class ServicePane extends Component {
}
-export default ServicePane;
+export default ServicesPane;
diff --git a/frontend/src/components/panels/ServicePane.scss b/frontend/src/components/panels/ServicesPane.scss
index daf7e79..daf7e79 100644
--- a/frontend/src/components/panels/ServicePane.scss
+++ b/frontend/src/components/panels/ServicesPane.scss
diff --git a/frontend/src/components/panels/StreamsPane.js b/frontend/src/components/panels/StreamsPane.js
new file mode 100644
index 0000000..9470d7d
--- /dev/null
+++ b/frontend/src/components/panels/StreamsPane.js
@@ -0,0 +1,241 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+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 {downloadBlob, getHeaderValue} from "../../utils";
+import ButtonField from "../fields/ButtonField";
+import ChoiceField from "../fields/ChoiceField";
+import MessageAction from "../objects/MessageAction";
+import "./StreamsPane.scss";
+
+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: [], 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});
+ }
+ };
+
+ tryParseConnectionMessage = (connectionMessage) => {
+ if (connectionMessage.metadata == null) {
+ return connectionMessage.content;
+ }
+ if (connectionMessage["is_metadata_continuation"]) {
+ return <span style={{"fontSize": "12px"}}>**already parsed in previous messages**</span>;
+ }
+
+ let unrollMap = (obj) => obj == null ? null : Object.entries(obj).map(([key, value]) =>
+ <p key={key}><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"
+ rel="noopener noreferrer">{m.host}{m.url}</a></u></i>;
+ return <span className="type-http-request">
+ <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":
+ const contentType = getHeaderValue(m, "Content-Type");
+ let body = m.body;
+ if (contentType && contentType.includes("application/json")) {
+ try {
+ const json = JSON.parse(m.body);
+ body = <ReactJson src={json} theme="grayscale" collapsed={false} displayDataTypes={false}/>;
+ } catch (e) {
+ log.error(e);
+ }
+ }
+
+ return <span className="type-http-response">
+ <p style={{"marginBottom": "7px"}}>{m.protocol} <strong>{m.status}</strong></p>
+ {unrollMap(m.headers)}
+ <div style={{"margin": "20px 0"}}>{body}</div>
+ {unrollMap(m.trailers)}
+ </span>;
+ 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]) =>
+ <ButtonField small key={actionName + "_button"} name={actionName} onClick={() => {
+ this.setState({
+ messageActionDialog: <MessageAction actionName={actionName} actionValue={actionValue}
+ onHide={() => this.setState({messageActionDialog: null})}/>
+ });
+ }}/>
+ );
+ case "http-response":
+ const contentType = getHeaderValue(m, "Content-Type");
+
+ if (contentType && contentType.includes("text/html")) {
+ return <ButtonField small name="render_html" onClick={() => {
+ 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) =>
+ <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="message-content">
+ {this.state.tryParse && this.state.format === "default" ? this.tryParseConnectionMessage(c) : c.content}
+ </div>
+ </div>
+ );
+
+ return (
+ <div className="pane-container stream-pane">
+ <div className="stream-pane-header container-fluid">
+ <Row>
+ <div className="header-info col">
+ <span><strong>flow</strong>: {conn["ip_src"]}:{conn["port_src"]} -> {conn["ip_dst"]}:{conn["port_dst"]}</span>
+ <span> | <strong>timestamp</strong>: {conn["started_at"]}</span>
+ </div>
+ <div className="header-actions col-auto">
+ <ChoiceField name="format" inline small onlyName
+ keys={["default", "hex", "hexdump", "base32", "base64", "ascii", "binary", "decimal", "octal"]}
+ values={["plain", "hex", "hexdump", "base32", "base64", "ascii", "binary", "decimal", "octal"]}
+ onChange={this.setFormat} value={this.state.value}/>
+
+ <ChoiceField name="view_as" inline small onlyName keys={["default"]} values={["default"]}/>
+
+ <ChoiceField name="download_as" inline small onlyName onChange={this.downloadStreamRaw}
+ keys={["nl_separated", "only_client", "only_server", "pwntools"]}
+ values={["nl_separated", "only_client", "only_server", "pwntools"]}/>
+ </div>
+ </Row>
+ </div>
+
+ <pre>{payload}</pre>
+ {this.state.messageActionDialog}
+ </div>
+ );
+ }
+}
+
+
+export default StreamsPane;
diff --git a/frontend/src/components/ConnectionContent.scss b/frontend/src/components/panels/StreamsPane.scss
index c97a4b0..1f641f3 100644
--- a/frontend/src/components/ConnectionContent.scss
+++ b/frontend/src/components/panels/StreamsPane.scss
@@ -1,8 +1,6 @@
-@import "../colors.scss";
+@import "../../colors";
-.connection-content {
- height: 100%;
- padding: 10px 10px 0;
+.stream-pane {
background-color: $color-primary-0;
pre {
@@ -16,6 +14,10 @@
margin: 0;
padding: 0;
}
+
+ &:hover::-webkit-scrollbar-thumb {
+ background: $color-secondary-2;
+ }
}
.connection-message {
@@ -48,6 +50,10 @@
.message-content {
padding: 10px;
+
+ .react-json-view {
+ background-color: inherit !important;
+ }
}
&:hover .connection-message-actions {
@@ -84,15 +90,15 @@
}
}
- .connection-content-header {
+ .stream-pane-header {
height: 33px;
padding: 0;
- background-color: $color-primary-2;
+ background-color: $color-primary-3;
.header-info {
font-size: 12px;
padding-top: 7px;
- padding-left: 20px;
+ padding-left: 25px;
}
.header-actions {
@@ -100,6 +106,10 @@
.choice-field {
margin-top: -5px;
+
+ .field-value {
+ background-color: $color-primary-3;
+ }
}
}
}
diff --git a/frontend/src/components/panels/common.scss b/frontend/src/components/panels/common.scss
index 121a917..335e65b 100644
--- a/frontend/src/components/panels/common.scss
+++ b/frontend/src/components/panels/common.scss
@@ -2,11 +2,9 @@
.pane-container {
height: 100%;
- padding: 10px 10px 0;
background-color: $color-primary-3;
.pane-section {
- margin-bottom: 10px;
background-color: $color-primary-0;
.section-header {
@@ -14,7 +12,7 @@
font-weight: 500;
display: flex;
padding: 5px 10px;
- background-color: $color-primary-2;
+ background-color: $color-primary-3;
.api-request {
flex: 1;
@@ -34,6 +32,10 @@
margin-left: 10px;
color: $color-secondary-0;
}
+
+ &:hover::-webkit-scrollbar-thumb {
+ background: $color-secondary-2;
+ }
}
table {
@@ -98,4 +100,8 @@
margin-left: 5px;
}
}
+
+ &:hover::-webkit-scrollbar-thumb {
+ background: $color-secondary-2;
+ }
}
diff --git a/frontend/src/dispatcher.js b/frontend/src/dispatcher.js
new file mode 100644
index 0000000..32f3f33
--- /dev/null
+++ b/frontend/src/dispatcher.js
@@ -0,0 +1,57 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+const _ = require("lodash");
+
+class Dispatcher {
+
+ constructor() {
+ this.listeners = [];
+ }
+
+ dispatch = (topic, payload) => {
+ this.listeners.filter((l) => l.topic === topic).forEach((l) => l.callback(payload));
+ };
+
+ register = (topic, callback) => {
+ if (typeof callback !== "function") {
+ throw new Error("dispatcher callback must be a function");
+ }
+ if (typeof topic === "string") {
+ this.listeners.push({topic, callback});
+ } else if (typeof topic === "object" && Array.isArray(topic)) {
+ topic.forEach((e) => {
+ if (typeof e !== "string") {
+ throw new Error("all topics must be strings");
+ }
+ });
+
+ topic.forEach((e) => this.listeners.push({e, callback}));
+ } else {
+ throw new Error("topic must be a string or an array of strings");
+ }
+ };
+
+ unregister = (callback) => {
+ _.remove(this.listeners, (l) => l.callback === callback);
+ };
+
+}
+
+const dispatcher = new Dispatcher();
+
+export default dispatcher;
diff --git a/frontend/src/index.js b/frontend/src/index.js
index 2e90371..62cb974 100644
--- a/frontend/src/index.js
+++ b/frontend/src/index.js
@@ -1,18 +1,32 @@
-import React from 'react';
-import ReactDOM from 'react-dom';
-import 'bootstrap/dist/css/bootstrap.css';
-import './index.scss';
-import App from './views/App';
-import * as serviceWorker from './serviceWorker';
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import "bootstrap/dist/css/bootstrap.css";
+import React from "react";
+import ReactDOM from "react-dom";
+import App from "./components/App";
+import "./index.scss";
+import notifications from "./notifications";
+
+notifications.createWebsocket();
ReactDOM.render(
- <React.StrictMode>
- <App />
- </React.StrictMode>,
- document.getElementById('root')
+ // <React.StrictMode>
+ <App/>,
+ // </React.StrictMode>,
+ document.getElementById("root")
);
-
-// If you want your app to work offline and load faster, you can change
-// unregister() to register() below. Note this comes with some pitfalls.
-// Learn more about service workers: https://bit.ly/CRA-PWA
-serviceWorker.unregister();
diff --git a/frontend/src/index.scss b/frontend/src/index.scss
index 2e5b6b9..1378d81 100644
--- a/frontend/src/index.scss
+++ b/frontend/src/index.scss
@@ -1,7 +1,9 @@
@import url("https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500&display=swap");
@import-normalize ;
+
@import "colors.scss";
+
body {
font-family: "Fira Code", monospace;
font-size: 100%;
@@ -75,3 +77,17 @@ a {
.popover-header {
color: $color-primary-1;
}
+
+@keyframes pulse {
+ 0% {
+ opacity: 1;
+ }
+
+ 50% {
+ opacity: 0.3;
+ }
+
+ 100% {
+ opacity: 1;
+ }
+}
diff --git a/frontend/src/log.js b/frontend/src/log.js
new file mode 100644
index 0000000..9a75fbc
--- /dev/null
+++ b/frontend/src/log.js
@@ -0,0 +1,29 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+const log = {
+ debug: (...obj) => isDevelopment() && console.info(...obj),
+ info: (...obj) => console.info(...obj),
+ warn: (...obj) => console.warn(...obj),
+ error: (...obj) => console.error(obj)
+};
+
+function isDevelopment() {
+ return !process.env.NODE_ENV || process.env.NODE_ENV === "development";
+}
+
+export default log;
diff --git a/frontend/src/logo.svg b/frontend/src/logo.svg
index 6b60c10..cb825f3 100644
--- a/frontend/src/logo.svg
+++ b/frontend/src/logo.svg
@@ -1,7 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
- <g fill="#61DAFB">
- <path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
- <circle cx="420.9" cy="296.5" r="45.7"/>
- <path d="M520.5 78.1z"/>
- </g>
-</svg>
+<svg height="512pt" viewBox="0 0 512.0001 512" width="512pt" xmlns="http://www.w3.org/2000/svg"><path d="m99 163h140v209h-140zm0 0" fill="#5aa096"/><path d="m10 372v-40h90l40 40h292l70-30-46.113281 75.878906c-14.972657 21.382813-39.433594 34.121094-65.539063 34.121094h-300.347656c-44.183594 0-80-35.816406-80-80zm0 0" fill="#96786e"/><path d="m320.246094 466.644531-35.355469 35.355469-45.960937-45.960938c-9.765626-9.765624-9.765626-25.59375 0-35.355468 9.761718-9.761719 25.589843-9.761719 35.355468 0zm0 0" fill="#5aa096"/><path d="m232.734375 132.625c11.335937-21.507812 10.265625-41.625 10.265625-41.625 0-46.945312-28.339844-81-67-81s-70 38.054688-70 85v37.144531c-14.839844 1.582031-41.0625 10.261719-41.0625 51.855469v107s35-49.5 53.5-49.5 30.5 21.5 30.5 21.5-6-50.5 23.5-50.5c42.5625 0 14.5625 103.5 50.5 103.5 31.0625 0 25-73 25-73l23 13v-75s4.351562-42.707031-38.203125-48.375zm0 0" fill="#96786e"/><path d="m201 85c0-19.328125-11.191406-35-25-35s-25 15.671875-25 35 11.191406 35 25 35 25-15.671875 25-35zm0 0" fill="#f0e6d2"/><path d="m509.554688 335.445312c-2.851563-3.285156-7.496094-4.347656-11.492188-2.636718l-68.117188 29.191406h-180.945312v-53.214844c7.375-13.921875 9.21875-33.839844 9.4375-48.363281l7.578125 4.28125c3.097656 1.75 6.890625 1.726563 9.960937-.066406 3.070313-1.792969 4.960938-5.082031 4.960938-8.636719v-74.570312c.21875-2.851563.703125-13.9375-3.773438-25.976563-3.972656-10.691406-12.425781-23.59375-30.378906-29.789063 6.328125-17.105468 6.316406-31.453124 6.222656-34.597656-.003906-.101562-.003906-.191406-.007812-.269531-.042969-24.925781-7.605469-47.75-21.304688-64.28125-14.167968-17.097656-33.949218-26.515625-55.695312-26.515625-44.113281 0-80 42.617188-80 95v28.789062c-7.277344 1.835938-15.53125 5.320313-22.785156 11.847657-12.128906 10.910156-18.277344 27.183593-18.277344 48.363281v15.785156l-37.480469-41.941406c-3.679687-4.117188-10.003906-4.476562-14.117187-.792969-4.121094 3.679688-4.476563 10.003907-.792969 14.121094l52.390625 58.628906v61.199219c0 4.355469 2.820312 8.210938 6.972656 9.53125s8.679688-.203125 11.195313-3.757812c.074219-.105469 6.878906-9.703126 15.894531-20.199219v45.425781h-79c-5.523438 0-10 4.476562-10 10v40c0 49.625 40.375 90 90 90h140.804688c.34375.375 47.015624 47.070312 47.015624 47.070312 1.953126 1.953126 4.511719 2.929688 7.070313 2.929688s5.117187-.976562 7.070313-2.929688l35.355468-35.355468c3.171875-3.167969 3.765625-7.941406 1.789063-11.714844h61.242187c29.339844 0 56.90625-14.351562 73.730469-38.390625.121094-.175781.238281-.355469.351563-.539063l46.117187-75.878906c2.257813-3.714844 1.855469-8.460937-.992187-11.746094zm-365.410157 26.554688-35.144531-35.140625v-36.5625l64.074219 71.703125zm-35.144531-101.71875v-3.59375c6.601562-5.085938 9.394531-5.1875 9.4375-5.1875 10.222656 0 19.191406 11.84375 21.769531 16.375 2.324219 4.164062 7.265625 6.097656 11.804688 4.636719 4.535156-1.464844 7.410156-5.941407 6.859375-10.675781-1.167969-10.050782-.550782-28.765626 6.070312-36.195313 1.933594-2.171875 4.246094-3.140625 7.496094-3.140625 12.25 0 15.25 19.105469 18.148438 46.097656 1.410156 13.171875 2.746093 25.613282 6.238281 35.710938 6.195312 17.929687 17.621093 21.691406 26.113281 21.691406 2.082031 0 4.109375-.226562 6.0625-.660156v36.660156h-29.101562zm-31.703125-.695312c-.800781.894531-1.585937 1.785156-2.359375 2.671874v-78.257812c0-20.65625 7.078125-33.828125 21.0625-39.3125v18.3125c0 5.511719 4.492188 10 10 10 5.511719 0 10-4.488281 10-10v-68c0-41.355469 26.917969-75 60-75 33.027344 0 57 29.859375 57 71 0 .148438.007812.339844.011719.492188.003906.015624.007812.207031.011719.546874 0 .097657.003906.203126 0 .328126v.121093c0 .15625 0 .328125-.003907.523438v.0625c-.121093 6.808593-2.050781 34.238281-24.488281 54.847656-4.066406 3.734375-4.335938 10.0625-.597656 14.128906 1.96875 2.144531 4.664062 3.234375 7.367187 3.234375 2.414063 0 4.839844-.871094 6.761719-2.636718 6.515625-5.988282 11.652344-12.378907 15.722656-18.792969 25.035156 7.121093 23.488282 33.03125 23.203125 36.128906-.03125.335937-.050781 58.875-.050781 58.875l-8.078125-4.566406c-3.226563-1.820313-7.1875-1.710938-10.308594.285156-3.121093 1.996094-4.882812 5.554687-4.578125 9.242187 1.4375 17.550782.191406 49.21875-9.273437 59.492188-1.734375 1.882812-3.457031 2.6875-5.761719 2.6875-1.632812 0-4.367188 0-7.210938-8.226562-2.75-7.953126-3.96875-19.296876-5.257812-31.3125-1.503906-13.996094-3.058594-28.472657-7.386719-40.046876-4.296875-11.488281-10.609375-17.691406-17.082031-20.882812v-45.53125c0-5.523438-4.476562-10-10-10s-10 4.476562-10 10v43.125c-6.253906 1.257812-11.714844 4.390625-16.023438 9.242188-6.230468 7.015624-9.226562 16.765624-10.574218 25.941406-5.785156-3.898438-12.832032-6.808594-20.964844-6.808594-6.882812 0-18.636719 2.910156-41.140625 28.085938zm12.703125 182.414062c-38.597656 0-70-31.402344-70-70v-30h75.855469l20 20h-25.855469c-5.511719 0-10 4.488281-10 10s4.488281 10 10 10h100.949219l17.871093 20h-9.820312c-5.523438 0-10 4.476562-10 10s4.476562 10 10 10h26.664062c-3.273437 6.199219-4.554687 13.183594-3.847656 20zm194.890625 45.859375-38.890625-38.890625c-5.847656-5.851562-5.847656-15.367188-.003906-21.214844 5.558594-5.558594 15.46875-5.75 21.214844 0l38.890624 38.890625zm162.625-75.460937c-13.105469 18.539062-34.453125 29.601562-57.167969 29.601562h-80.605468l-20-20h29.257812c5.523438 0 10-4.476562 10-10s-4.476562-10-10-10h-83.355469l-17.875-20h214.230469c1.355469 0 2.695312-.273438 3.9375-.808594l41.296875-17.699218zm0 0"/><path d="m50 362c-5.511719 0-10 4.488281-10 10s4.488281 10 10 10 10-4.488281 10-10-4.488281-10-10-10zm0 0"/><path d="m387 402h-28c-5.523438 0-10 4.476562-10 10s4.476562 10 10 10h28c5.523438 0 10-4.476562 10-10s-4.476562-10-10-10zm0 0"/><path d="m159 402h-30c-5.523438 0-10 4.476562-10 10s4.476562 10 10 10h30c5.523438 0 10-4.476562 10-10s-4.476562-10-10-10zm0 0"/><path d="m176 130c19.625 0 35-19.765625 35-45s-15.375-45-35-45-35 19.765625-35 45 15.375 45 35 45zm0-70c7.09375 0 15 10.265625 15 25s-7.90625 25-15 25-15-10.265625-15-25 7.90625-25 15-25zm0 0"/><path d="m106 193c-5.511719 0-10 4.488281-10 10s4.488281 10 10 10 10-4.488281 10-10-4.488281-10-10-10zm0 0"/></svg> \ No newline at end of file
diff --git a/frontend/src/notifications.js b/frontend/src/notifications.js
new file mode 100644
index 0000000..3c83b87
--- /dev/null
+++ b/frontend/src/notifications.js
@@ -0,0 +1,57 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import dispatcher from "./dispatcher";
+import log from "./log";
+
+class Notifications {
+
+ constructor() {
+ const location = document.location;
+ this.wsUrl = `ws://${location.hostname}${location.port ? ":" + location.port : ""}/ws`;
+ }
+
+ createWebsocket = () => {
+ this.ws = new WebSocket(this.wsUrl);
+ this.ws.onopen = this.onWebsocketOpen;
+ this.ws.onerror = this.onWebsocketError;
+ this.ws.onclose = this.onWebsocketClose;
+ this.ws.onmessage = this.onWebsocketMessage;
+ };
+
+ onWebsocketOpen = () => {
+ log.debug("Connected to backend with websocket");
+ };
+
+ onWebsocketError = (err) => {
+ this.ws.close();
+ log.error("Websocket error", err);
+ setTimeout(() => this.createWebsocket(), 3000);
+ };
+
+ onWebsocketClose = () => {
+ log.debug("Closed websocket connection with backend");
+ };
+
+ onWebsocketMessage = (message) => {
+ dispatcher.dispatch("notifications", JSON.parse(message.data));
+ };
+}
+
+const notifications = new Notifications();
+
+export default notifications;
diff --git a/frontend/src/serviceWorker.js b/frontend/src/serviceWorker.js
deleted file mode 100644
index b04b771..0000000
--- a/frontend/src/serviceWorker.js
+++ /dev/null
@@ -1,141 +0,0 @@
-// This optional code is used to register a service worker.
-// register() is not called by default.
-
-// This lets the app load faster on subsequent visits in production, and gives
-// it offline capabilities. However, it also means that developers (and users)
-// will only see deployed updates on subsequent visits to a page, after all the
-// existing tabs open on the page have been closed, since previously cached
-// resources are updated in the background.
-
-// To learn more about the benefits of this model and instructions on how to
-// opt-in, read https://bit.ly/CRA-PWA
-
-const isLocalhost = Boolean(
- window.location.hostname === 'localhost' ||
- // [::1] is the IPv6 localhost address.
- window.location.hostname === '[::1]' ||
- // 127.0.0.0/8 are considered localhost for IPv4.
- window.location.hostname.match(
- /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
- )
-);
-
-export function register(config) {
- if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
- // The URL constructor is available in all browsers that support SW.
- const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
- if (publicUrl.origin !== window.location.origin) {
- // Our service worker won't work if PUBLIC_URL is on a different origin
- // from what our page is served on. This might happen if a CDN is used to
- // serve assets; see https://github.com/facebook/create-react-app/issues/2374
- return;
- }
-
- window.addEventListener('load', () => {
- const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
-
- if (isLocalhost) {
- // This is running on localhost. Let's check if a service worker still exists or not.
- checkValidServiceWorker(swUrl, config);
-
- // Add some additional logging to localhost, pointing developers to the
- // service worker/PWA documentation.
- navigator.serviceWorker.ready.then(() => {
- console.log(
- 'This web app is being served cache-first by a service ' +
- 'worker. To learn more, visit https://bit.ly/CRA-PWA'
- );
- });
- } else {
- // Is not localhost. Just register service worker
- registerValidSW(swUrl, config);
- }
- });
- }
-}
-
-function registerValidSW(swUrl, config) {
- navigator.serviceWorker
- .register(swUrl)
- .then(registration => {
- registration.onupdatefound = () => {
- const installingWorker = registration.installing;
- if (installingWorker == null) {
- return;
- }
- installingWorker.onstatechange = () => {
- if (installingWorker.state === 'installed') {
- if (navigator.serviceWorker.controller) {
- // At this point, the updated precached content has been fetched,
- // but the previous service worker will still serve the older
- // content until all client tabs are closed.
- console.log(
- 'New content is available and will be used when all ' +
- 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
- );
-
- // Execute callback
- if (config && config.onUpdate) {
- config.onUpdate(registration);
- }
- } else {
- // At this point, everything has been precached.
- // It's the perfect time to display a
- // "Content is cached for offline use." message.
- console.log('Content is cached for offline use.');
-
- // Execute callback
- if (config && config.onSuccess) {
- config.onSuccess(registration);
- }
- }
- }
- };
- };
- })
- .catch(error => {
- console.error('Error during service worker registration:', error);
- });
-}
-
-function checkValidServiceWorker(swUrl, config) {
- // Check if the service worker can be found. If it can't reload the page.
- fetch(swUrl, {
- headers: { 'Service-Worker': 'script' },
- })
- .then(response => {
- // Ensure service worker exists, and that we really are getting a JS file.
- const contentType = response.headers.get('content-type');
- if (
- response.status === 404 ||
- (contentType != null && contentType.indexOf('javascript') === -1)
- ) {
- // No service worker found. Probably a different app. Reload the page.
- navigator.serviceWorker.ready.then(registration => {
- registration.unregister().then(() => {
- window.location.reload();
- });
- });
- } else {
- // Service worker found. Proceed as normal.
- registerValidSW(swUrl, config);
- }
- })
- .catch(() => {
- console.log(
- 'No internet connection found. App is running in offline mode.'
- );
- });
-}
-
-export function unregister() {
- if ('serviceWorker' in navigator) {
- navigator.serviceWorker.ready
- .then(registration => {
- registration.unregister();
- })
- .catch(error => {
- console.error(error.message);
- });
- }
-}
diff --git a/frontend/src/setupProxy.js b/frontend/src/setupProxy.js
new file mode 100644
index 0000000..fb60b75
--- /dev/null
+++ b/frontend/src/setupProxy.js
@@ -0,0 +1,24 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+const {createProxyMiddleware} = require("http-proxy-middleware");
+
+module.exports = function (app) {
+ app.use(createProxyMiddleware("/api", {target: "http://localhost:3333"}));
+ app.use(createProxyMiddleware("/setup", {target: "http://localhost:3333"}));
+ app.use(createProxyMiddleware("/ws", {target: "http://localhost:3333", ws: true}));
+};
diff --git a/frontend/src/setupTests.js b/frontend/src/setupTests.js
deleted file mode 100644
index 74b1a27..0000000
--- a/frontend/src/setupTests.js
+++ /dev/null
@@ -1,5 +0,0 @@
-// jest-dom adds custom jest matchers for asserting on DOM nodes.
-// allows you to do things like:
-// expect(element).toHaveTextContent(/react/i)
-// learn more: https://github.com/testing-library/jest-dom
-import '@testing-library/jest-dom/extend-expect';
diff --git a/frontend/src/utils.js b/frontend/src/utils.js
index 8c7fe0f..445e576 100644
--- a/frontend/src/utils.js
+++ b/frontend/src/utils.js
@@ -1,15 +1,32 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
const timeRegex = /^(0[0-9]|1[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$/;
export function createCurlCommand(subCommand, method = null, json = null, data = null) {
- const full = window.location.protocol + '//' + window.location.hostname + (window.location.port ? ':' + window.location.port : '');
+ const full = window.location.protocol + "//" + window.location.hostname + (window.location.port ? ":" + window.location.port : "");
let contentType = null;
let content = null;
if (json != null) {
- contentType = ' -H "Content-Type: application/json" \\\n';
+ contentType = " -H \"Content-Type: application/json\" \\\n";
content = ` -d '${JSON.stringify(json)}'`;
} else if (data != null) {
- contentType = ' -H "Content-Type: multipart/form-data" \\\n';
+ contentType = " -H \"Content-Type: multipart/form-data\" \\\n";
content = " " + Object.entries(data).map(([key, value]) => `-F "${key}=${value}"`).join(" \\\n ");
}
@@ -49,13 +66,13 @@ export function timeToTimestamp(time) {
let d = new Date();
let matches = time.match(timeRegex);
- if (matches[1] !== undefined) {
+ if (matches[1]) {
d.setHours(matches[1]);
}
- if (matches[2] !== undefined) {
+ if (matches[2]) {
d.setMinutes(matches[2]);
}
- if (matches[3] !== undefined) {
+ if (matches[3]) {
d.setSeconds(matches[3]);
}
@@ -67,7 +84,7 @@ export function timestampToTime(timestamp) {
let hours = d.getHours();
let minutes = "0" + d.getMinutes();
let seconds = "0" + d.getSeconds();
- return hours + ':' + minutes.substr(-2) + ':' + seconds.substr(-2);
+ return hours + ":" + minutes.substr(-2) + ":" + seconds.substr(-2);
}
export function timestampToDateTime(timestamp) {
@@ -83,7 +100,7 @@ export function dateTimeToTime(dateTime) {
let hours = dateTime.getHours();
let minutes = "0" + dateTime.getMinutes();
let seconds = "0" + dateTime.getSeconds();
- return hours + ':' + minutes.substr(-2) + ':' + seconds.substr(-2);
+ return hours + ":" + minutes.substr(-2) + ":" + seconds.substr(-2);
}
export function durationBetween(from, to) {
@@ -111,3 +128,37 @@ export function formatSize(size) {
export function randomClassName() {
return Math.random().toString(36).slice(2);
}
+
+export function getHeaderValue(request, key) {
+ if (request && request.headers) {
+ return request.headers[Object.keys(request.headers).find((k) => k.toLowerCase() === key.toLowerCase())];
+ }
+ return null;
+}
+
+export function downloadBlob(blob, fileName) {
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.style.display = "none";
+ a.href = url;
+ a.download = fileName;
+ document.body.appendChild(a);
+ a.click();
+ window.URL.revokeObjectURL(url);
+}
+
+export function updateParams(urlParams, payload) {
+ const params = new URLSearchParams(urlParams.toString());
+ Object.entries(payload).forEach(([key, value]) => {
+ if (value == null) {
+ params.delete(key);
+ } else if (Array.isArray(value)) {
+ params.delete(key);
+ value.forEach((v) => params.append(key, v));
+ } else {
+ params.set(key, value);
+ }
+ });
+
+ return params;
+}
diff --git a/frontend/src/validation.js b/frontend/src/validation.js
index 7089d7f..87b08de 100644
--- a/frontend/src/validation.js
+++ b/frontend/src/validation.js
@@ -1,3 +1,19 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
const validation = {
isValidColor: (color) => /^#(?:[0-9a-fA-F]{3}){1,2}$/.test(color),
diff --git a/frontend/src/views/App.js b/frontend/src/views/App.js
deleted file mode 100644
index fb4454c..0000000
--- a/frontend/src/views/App.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import React, {Component} from 'react';
-import './App.scss';
-import Header from "./Header";
-import MainPane from "../components/panels/MainPane";
-import Footer from "./Footer";
-import {BrowserRouter as Router} from "react-router-dom";
-import Filters from "./Filters";
-import backend from "../backend";
-import ConfigurationPane from "../components/panels/ConfigurationPane";
-
-class App extends Component {
-
- constructor(props) {
- super(props);
-
- this.state = {};
- }
-
- componentDidMount() {
- backend.get("/api/services").then(_ => this.setState({configured: true}));
- }
-
- render() {
- let modal;
- if (this.state.filterWindowOpen && this.state.configured) {
- modal = <Filters onHide={() => this.setState({filterWindowOpen: false})}/>;
- }
-
- return (
- <div className="main">
- <Router>
- <div className="main-header">
- <Header onOpenFilters={() => this.setState({filterWindowOpen: true})} />
- </div>
- <div className="main-content">
- {this.state.configured ? <MainPane /> :
- <ConfigurationPane onConfigured={() => this.setState({configured: true})} />}
- {modal}
- </div>
- <div className="main-footer">
- {this.state.configured && <Footer/>}
- </div>
- </Router>
- </div>
- );
- }
-}
-
-export default App;
diff --git a/frontend/src/views/App.scss b/frontend/src/views/App.scss
deleted file mode 100644
index 5c5bd99..0000000
--- a/frontend/src/views/App.scss
+++ /dev/null
@@ -1,15 +0,0 @@
-
-.main {
- display: flex;
- flex-direction: column;
- height: 100vh;
-
- .main-content {
- overflow: hidden;
- flex: 1 1;
- }
-
- .main-header, .main-footer {
- flex: 0 0;
- }
-}
diff --git a/frontend/src/views/Connections.js b/frontend/src/views/Connections.js
deleted file mode 100644
index 73979c4..0000000
--- a/frontend/src/views/Connections.js
+++ /dev/null
@@ -1,201 +0,0 @@
-import React, {Component} from 'react';
-import './Connections.scss';
-import Connection from "../components/Connection";
-import Table from 'react-bootstrap/Table';
-import {Redirect} from 'react-router';
-import {withRouter} from "react-router-dom";
-import backend from "../backend";
-import ConnectionMatchedRules from "../components/ConnectionMatchedRules";
-
-class Connections extends Component {
-
- constructor(props) {
- super(props);
- this.state = {
- loading: false,
- connections: [],
- firstConnection: null,
- lastConnection: null,
- prevParams: null,
- flagRule: null,
- rules: null,
- queryString: null
- };
-
- this.scrollTopThreashold = 0.00001;
- this.scrollBottomThreashold = 0.99999;
- this.maxConnections = 500;
- this.queryLimit = 50;
- }
-
- componentDidMount() {
- this.loadConnections({limit: this.queryLimit})
- .then(() => this.setState({loaded: true}));
- if (this.props.initialConnection != null) {
- this.setState({selected: this.props.initialConnection.id});
- // TODO: scroll to initial connection
- }
- }
-
- connectionSelected = (c) => {
- this.setState({selected: c.id});
- this.props.onSelected(c);
- };
-
- componentDidUpdate(prevProps, prevState, snapshot) {
- if (this.state.loaded && prevProps.location.search !== this.props.location.search) {
- this.setState({queryString: this.props.location.search});
- this.loadConnections({limit: this.queryLimit})
- .then(() => console.log("Connections reloaded after query string update"));
- }
- }
-
- handleScroll = (e) => {
- let relativeScroll = e.currentTarget.scrollTop / (e.currentTarget.scrollHeight - e.currentTarget.clientHeight);
- if (!this.state.loading && relativeScroll > this.scrollBottomThreashold) {
- this.loadConnections({from: this.state.lastConnection.id, limit: this.queryLimit,})
- .then(() => console.log("Following connections loaded"));
- }
- if (!this.state.loading && relativeScroll < this.scrollTopThreashold) {
- this.loadConnections({to: this.state.firstConnection.id, limit: this.queryLimit,})
- .then(() => console.log("Previous connections loaded"));
- }
- };
-
- addServicePortFilter = (port) => {
- const urlParams = new URLSearchParams(this.props.location.search);
- urlParams.set("service_port", port);
- this.setState({queryString: "?" + urlParams});
- };
-
- addMatchedRulesFilter = (matchedRule) => {
- const urlParams = new URLSearchParams(this.props.location.search);
- const oldMatchedRules = urlParams.getAll("matched_rules") || [];
-
- if (!oldMatchedRules.includes(matchedRule)) {
- urlParams.append("matched_rules", matchedRule);
- this.setState({queryString: "?" + urlParams});
- }
- };
-
- async loadConnections(params) {
- let url = "/api/connections";
- const urlParams = new URLSearchParams(this.props.location.search);
- for (const [name, value] of Object.entries(params)) {
- urlParams.set(name, value);
- }
-
- this.setState({loading: true, prevParams: params});
- let res = (await backend.get(`${url}?${urlParams}`)).json;
-
- let connections = this.state.connections;
- let firstConnection = this.state.firstConnection;
- let lastConnection = this.state.lastConnection;
-
- if (params !== undefined && params.from !== undefined) {
- if (res.length > 0) {
- connections = this.state.connections.concat(res);
- lastConnection = connections[connections.length - 1];
- if (connections.length > this.maxConnections) {
- connections = connections.slice(connections.length - this.maxConnections,
- connections.length - 1);
- firstConnection = connections[0];
- }
- }
- } else if (params !== undefined && params.to !== undefined) {
- if (res.length > 0) {
- connections = res.concat(this.state.connections);
- firstConnection = connections[0];
- if (connections.length > this.maxConnections) {
- connections = connections.slice(0, this.maxConnections);
- lastConnection = connections[this.maxConnections - 1];
- }
- }
- } else {
- if (res.length > 0) {
- connections = res;
- firstConnection = connections[0];
- lastConnection = connections[connections.length - 1];
- } else {
- connections = [];
- firstConnection = null;
- lastConnection = null;
- }
- }
-
- let rules = this.state.rules;
- if (rules === null) {
- rules = (await backend.get("/api/rules")).json;
- }
-
- this.setState({
- loading: false,
- connections: connections,
- rules: rules,
- firstConnection: firstConnection,
- lastConnection: lastConnection
- });
- }
-
- render() {
- let redirect;
- let queryString = this.state.queryString !== null ? this.state.queryString : "";
- if (this.state.selected) {
- let format = this.props.match.params.format;
- format = format !== undefined ? "/" + format : "";
- redirect = <Redirect push to={`/connections/${this.state.selected}${format}${queryString}`} />;
- }
-
- let loading = null;
- if (this.state.loading) {
- loading = <tr>
- <td colSpan={9}>Loading...</td>
- </tr>;
- }
-
- return (
- <div className="connections" onScroll={this.handleScroll}>
- <div className="connections-header-padding"/>
- <Table borderless size="sm">
- <thead>
- <tr>
- <th>service</th>
- <th>srcip</th>
- <th>srcport</th>
- <th>dstip</th>
- <th>dstport</th>
- <th>started_at</th>
- <th>duration</th>
- <th>up</th>
- <th>down</th>
- <th>actions</th>
- </tr>
- </thead>
- <tbody>
- {
- this.state.connections.flatMap(c => {
- return [<Connection key={c.id} data={c} onSelected={() => this.connectionSelected(c)}
- selected={this.state.selected === c.id}
- onMarked={marked => c.marked = marked}
- onEnabled={enabled => c.hidden = !enabled}
- addServicePortFilter={this.addServicePortFilter} />,
- c.matched_rules.length > 0 &&
- <ConnectionMatchedRules key={c.id + "_m"} matchedRules={c.matched_rules}
- rules={this.state.rules}
- addMatchedRulesFilter={this.addMatchedRulesFilter} />
- ];
- })
- }
- {loading}
- </tbody>
- </Table>
-
- {redirect}
- </div>
- );
- }
-
-}
-
-
-export default withRouter(Connections);
diff --git a/frontend/src/views/Connections.scss b/frontend/src/views/Connections.scss
deleted file mode 100644
index c7bd1df..0000000
--- a/frontend/src/views/Connections.scss
+++ /dev/null
@@ -1,37 +0,0 @@
-@import "../colors.scss";
-
-.connections {
- position: relative;
- overflow: auto;
- height: 100%;
- padding: 0 10px;
- background-color: $color-primary-3;
-
- .table {
- margin-top: 10px;
- }
-
- .connections-header-padding {
- position: sticky;
- top: 0;
- right: 0;
- left: 0;
- height: 10px;
- margin-bottom: -10px;
- background-color: $color-primary-3;
- }
-
- th {
- font-size: 13.5px;
- position: sticky;
- top: 10px;
- padding: 5px;
- border-top: 3px solid $color-primary-3;
- border-bottom: 3px solid $color-primary-3;
- background-color: $color-primary-2;
- }
-
- &:hover::-webkit-scrollbar-thumb {
- background: $color-secondary-2;
- }
-}
diff --git a/frontend/src/views/Filters.js b/frontend/src/views/Filters.js
deleted file mode 100644
index ba7d467..0000000
--- a/frontend/src/views/Filters.js
+++ /dev/null
@@ -1,99 +0,0 @@
-import React, {Component} from 'react';
-import {Col, Container, Modal, Row, Table} from "react-bootstrap";
-import {filtersDefinitions, filtersNames} from "../components/filters/FiltersDefinitions";
-import ButtonField from "../components/fields/ButtonField";
-
-class Filters extends Component {
-
- constructor(props) {
- super(props);
- let newState = {};
- filtersNames.forEach(elem => newState[`${elem}_active`] = false);
- this.state = newState;
- }
-
- componentDidMount() {
- let newState = {};
- filtersNames.forEach(elem => newState[`${elem}_active`] = localStorage.getItem(`filters.${elem}`) === "true");
- this.setState(newState);
- }
-
- checkboxChangesHandler(filterName, event) {
- this.setState({[`${filterName}_active`]: event.target.checked});
- localStorage.setItem(`filters.${filterName}`, event.target.checked);
- if (typeof window !== "undefined") {
- window.dispatchEvent(new Event("quick-filters"));
- }
- }
-
- generateRows(filtersNames) {
- return filtersNames.map(name =>
- <tr key={name}>
- <td><input type="checkbox"
- checked={this.state[`${name}_active`]}
- onChange={event => this.checkboxChangesHandler(name, event)} /></td>
- <td>{filtersDefinitions[name]}</td>
- </tr>
- );
- }
-
- render() {
- return (
- <Modal
- {...this.props}
- show="true"
- size="lg"
- aria-labelledby="filters-dialog"
- centered
- >
- <Modal.Header>
- <Modal.Title id="filters-dialog">
- ~/filters
- </Modal.Title>
- </Modal.Header>
- <Modal.Body>
- <Container>
- <Row>
- <Col md={6}>
- <Table borderless size="sm" className="filters-table">
- <thead>
- <tr>
- <th>show</th>
- <th>filter</th>
- </tr>
- </thead>
- <tbody>
- {this.generateRows(["service_port", "client_address", "min_duration",
- "min_bytes", "started_after", "closed_after", "marked"])}
- </tbody>
- </Table>
- </Col>
- <Col md={6}>
- <Table borderless size="sm" className="filters-table">
- <thead>
- <tr>
- <th>show</th>
- <th>filter</th>
- </tr>
- </thead>
- <tbody>
- {this.generateRows(["matched_rules", "client_port", "max_duration",
- "max_bytes", "started_before", "closed_before", "hidden"])}
- </tbody>
- </Table>
- </Col>
-
- </Row>
-
-
- </Container>
- </Modal.Body>
- <Modal.Footer className="dialog-footer">
- <ButtonField variant="red" bordered onClick={this.props.onHide} name="close" />
- </Modal.Footer>
- </Modal>
- );
- }
-}
-
-export default Filters;
diff --git a/frontend/src/views/Footer.js b/frontend/src/views/Footer.js
deleted file mode 100644
index 0a3c5a3..0000000
--- a/frontend/src/views/Footer.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import React, {Component} from 'react';
-import './Footer.scss';
-
-class Footer extends Component {
-
- render() {
- return (
- <footer className="footer container-fluid">
- <div className="row">
- <div className="col-12">
- <div className="footer-timeline">timeline - <a href="https://github.com/eciavatta/caronte/issues/12">to be implemented</a></div>
- </div>
- </div>
- </footer>
- );
- }
-}
-
-export default Footer;
diff --git a/frontend/src/views/Footer.scss b/frontend/src/views/Footer.scss
deleted file mode 100644
index 6ec4a62..0000000
--- a/frontend/src/views/Footer.scss
+++ /dev/null
@@ -1,13 +0,0 @@
-@import "../colors.scss";
-
-.footer {
- padding: 15px 30px;
-
- > .row {
- background-color: $color-primary-0;
- }
-
- .footer-timeline {
- height: 100px;
- }
-}
diff --git a/frontend/src/views/Header.js b/frontend/src/views/Header.js
deleted file mode 100644
index 944f1d5..0000000
--- a/frontend/src/views/Header.js
+++ /dev/null
@@ -1,92 +0,0 @@
-import React, {Component} from 'react';
-import Typed from 'typed.js';
-import './Header.scss';
-import {filtersDefinitions, filtersNames} from "../components/filters/FiltersDefinitions";
-import {Link} from "react-router-dom";
-import ButtonField from "../components/fields/ButtonField";
-
-class Header extends Component {
-
- constructor(props) {
- super(props);
- let newState = {};
- filtersNames.forEach(elem => newState[`${elem}_active`] = false);
- this.state = newState;
- this.fetchStateFromLocalStorage = this.fetchStateFromLocalStorage.bind(this);
- }
-
- componentDidMount() {
- const options = {
- strings: ["caronte$ "],
- typeSpeed: 50,
- cursorChar: "❚"
- };
- this.typed = new Typed(this.el, options);
-
- this.fetchStateFromLocalStorage();
-
- if (typeof window !== "undefined") {
- window.addEventListener("quick-filters", this.fetchStateFromLocalStorage);
- }
- }
-
- componentWillUnmount() {
- this.typed.destroy();
-
- if (typeof window !== "undefined") {
- window.removeEventListener("quick-filters", this.fetchStateFromLocalStorage);
- }
- }
-
- fetchStateFromLocalStorage() {
- let newState = {};
- filtersNames.forEach(elem => newState[`${elem}_active`] = localStorage.getItem(`filters.${elem}`) === "true");
- this.setState(newState);
- }
-
- render() {
- let quickFilters = filtersNames.filter(name => this.state[`${name}_active`])
- .map(name => <React.Fragment key={name} >{filtersDefinitions[name]}</React.Fragment>)
- .slice(0, 5);
-
- return (
- <header className="header container-fluid">
- <div className="row">
- <div className="col-auto">
- <h1 className="header-title type-wrap">
- <span style={{whiteSpace: 'pre'}} ref={(el) => {
- this.el = el;
- }}/>
- </h1>
- </div>
-
- <div className="col-auto">
- <div className="filters-bar">
- {quickFilters}
- </div>
- </div>
-
- <div className="col">
- <div className="header-buttons">
- <ButtonField variant="pink" onClick={this.props.onOpenFilters} name="filters" bordered />
- <Link to="/pcaps">
- <ButtonField variant="purple" name="pcaps" bordered />
- </Link>
- <Link to="/rules">
- <ButtonField variant="deep-purple" name="rules" bordered />
- </Link>
- <Link to="/services">
- <ButtonField variant="indigo" name="services" bordered />
- </Link>
- <Link to="/config">
- <ButtonField variant="blue" name="config" bordered />
- </Link>
- </div>
- </div>
- </div>
- </header>
- );
- }
-}
-
-export default Header;
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 6d111a3..e3cade9 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -909,6 +909,14 @@
"@babel/helper-create-regexp-features-plugin" "^7.10.4"
"@babel/helper-plugin-utils" "^7.10.4"
+"@babel/polyfill@^7.7.0":
+ version "7.11.5"
+ resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.11.5.tgz#df550b2ec53abbc2ed599367ec59e64c7a707bb5"
+ integrity sha512-FunXnE0Sgpd61pKSj2OSOs1D44rKTD3pGOfGilZ6LGrrIH0QEtJlTjqOqdF8Bs98JmjfGhni2BBkTfv9KcKJ9g==
+ dependencies:
+ core-js "^2.6.5"
+ regenerator-runtime "^0.13.4"
+
"@babel/preset-env@7.9.0":
version "7.9.0"
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.9.0.tgz#a5fc42480e950ae8f5d9f8f2bbc03f52722df3a8"
@@ -1648,6 +1656,13 @@
"@types/minimatch" "*"
"@types/node" "*"
+"@types/http-proxy@^1.17.4":
+ version "1.17.4"
+ resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.4.tgz#e7c92e3dbe3e13aa799440ff42e6d3a17a9d045b"
+ integrity sha512-IrSHl2u6AWXduUaDLqYpt45tLVCtYv7o4Z0s1KghBCDgIIS9oW5K1H8mZG/A2CfeLdEa7rTd1ACOiHBc1EMT2Q==
+ dependencies:
+ "@types/node" "*"
+
"@types/invariant@^2.2.33":
version "2.2.34"
resolved "https://registry.yarnpkg.com/@types/invariant/-/invariant-2.2.34.tgz#05e4f79f465c2007884374d4795452f995720bbe"
@@ -1725,9 +1740,9 @@
"@types/react" "*"
"@types/react@*", "@types/react@^16.9.11", "@types/react@^16.9.35":
- version "16.9.49"
- resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.49.tgz#09db021cf8089aba0cdb12a49f8021a69cce4872"
- integrity sha512-DtLFjSj0OYAdVLBbyjhuV9CdGVHCkHn2R+xr3XkBvK2rS1Y1tkc14XSGjYgm5Fjjr90AxH9tiSzc1pCFMGO06g==
+ version "16.9.50"
+ resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.50.tgz#cb5f2c22d42de33ca1f5efc6a0959feb784a3a2d"
+ integrity sha512-kPx5YsNnKDJejTk1P+lqThwxN2PczrocwsvqXnjvVvKpFescoY62ZiM3TV7dH1T8lFhlHZF+PE5xUyimUwqEGA==
dependencies:
"@types/prop-types" "*"
csstype "^3.0.2"
@@ -2268,6 +2283,11 @@ array-unique@^0.3.2:
resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
+array.prototype.fill@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/array.prototype.fill/-/array.prototype.fill-1.0.2.tgz#ab33207f21d57d1ab2f7f0d1cf122d3419c38ef5"
+ integrity sha1-qzMgfyHVfRqy9/DRzxItNBnDjvU=
+
array.prototype.flat@^1.2.1:
version "1.2.3"
resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz#0de82b426b0318dbfdb940089e38b043d37f6c7b"
@@ -2281,7 +2301,7 @@ arrify@^1.0.1:
resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=
-asap@~2.0.6:
+asap@~2.0.3, asap@~2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
@@ -2535,7 +2555,7 @@ babel-preset-react-app@^9.1.2:
babel-plugin-macros "2.8.0"
babel-plugin-transform-react-remove-prop-types "0.4.24"
-babel-runtime@^6.26.0:
+babel-runtime@^6.23.0, babel-runtime@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4=
@@ -2553,6 +2573,11 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
+base16@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/base16/-/base16-1.0.0.tgz#e297f60d7ec1014a7a971a39ebc8a98c0b681e70"
+ integrity sha1-4pf2DX7BAUp6lxo568ipjAtoHnA=
+
base64-js@^1.0.2:
version "1.3.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
@@ -2689,7 +2714,7 @@ braces@^2.3.1, braces@^2.3.2:
split-string "^3.0.2"
to-regex "^3.0.1"
-braces@~3.0.2:
+braces@^3.0.1, braces@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
@@ -2976,9 +3001,9 @@ caniuse-api@^3.0.0:
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001035, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001135:
- version "1.0.30001140"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001140.tgz#30dae27599f6ede2603a0962c82e468bca894232"
- integrity sha512-xFtvBtfGrpjTOxTpjP5F2LmN04/ZGfYV8EQzUIC/RmKpdrmzJrjqlJ4ho7sGuAMPko2/Jl08h7x9uObCfBFaAA==
+ version "1.0.30001142"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001142.tgz#a8518fdb5fee03ad95ac9f32a9a1e5999469c250"
+ integrity sha512-pDPpn9ankEpBFZXyCv2I4lh1v/ju+bqb78QfKf+w9XgDAFWBwSYPswXqprRdrgQWK0wQnpIbfwRjNHO1HWqvoQ==
capture-exit@^2.0.0:
version "2.0.0"
@@ -3246,6 +3271,11 @@ color@^3.0.0:
color-convert "^1.9.1"
color-string "^1.5.2"
+colorbrewer@^1.0.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/colorbrewer/-/colorbrewer-1.3.0.tgz#1d7e92a6277e42dc56377911bbd867bdbcb2ff7d"
+ integrity sha512-AzVPpWa+fuO/qY8LxPQjej6F49Lb2Cl+7U9YhPn6y4/SOY6u/EZiXUc7qHzRb6i6fWPStCUdEaU2731QyQKWjg==
+
colorette@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b"
@@ -3419,7 +3449,12 @@ core-js-pure@^3.0.0:
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813"
integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA==
-core-js@^2.4.0:
+core-js@^1.0.0:
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
+ integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=
+
+core-js@^2.4.0, core-js@^2.6.5:
version "2.6.11"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c"
integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==
@@ -3631,9 +3666,9 @@ css-what@2.1:
integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==
css-what@^3.2.1:
- version "3.3.0"
- resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.3.0.tgz#10fec696a9ece2e591ac772d759aacabac38cd39"
- integrity sha512-pv9JPyatiPaQ6pf4OvD/dbfm0o5LviWmwxNWzblYf/1u9QZd0ihV+PMwy5jdQWQ3349kZmKEx9WXuSka2dM4cg==
+ version "3.4.1"
+ resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.1.tgz#81cb70b609e4b1351b1e54cbc90fd9c54af86e2e"
+ integrity sha512-wHOppVDKl4vTAOWzJt5Ek37Sgd9qq1Bmj/T1OjvicWbU5W7ru7Pqbn0Jdqii3Drx/h+dixHKXNhZYx7blthL7g==
css.escape@^1.5.1:
version "1.5.1"
@@ -3769,6 +3804,123 @@ cyclist@^1.0.1:
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
+d3-array@^1.2.0:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f"
+ integrity sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==
+
+d3-axis@^1.0.8:
+ version "1.0.12"
+ resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-1.0.12.tgz#cdf20ba210cfbb43795af33756886fb3638daac9"
+ integrity sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ==
+
+d3-collection@1:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.7.tgz#349bd2aa9977db071091c13144d5e4f16b5b310e"
+ integrity sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==
+
+d3-color@1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.4.1.tgz#c52002bf8846ada4424d55d97982fef26eb3bc8a"
+ integrity sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==
+
+d3-dispatch@1:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.6.tgz#00d37bcee4dd8cd97729dd893a0ac29caaba5d58"
+ integrity sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==
+
+d3-ease@1, d3-ease@^1.0.3:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.7.tgz#9a834890ef8b8ae8c558b2fe55bd57f5993b85e2"
+ integrity sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ==
+
+d3-format@1, d3-format@^1.2.0:
+ version "1.4.5"
+ resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.4.5.tgz#374f2ba1320e3717eb74a9356c67daee17a7edb4"
+ integrity sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==
+
+d3-interpolate@1, d3-interpolate@^1.1.5:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.4.0.tgz#526e79e2d80daa383f9e0c1c1c7dcc0f0583e987"
+ integrity sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==
+ dependencies:
+ d3-color "1"
+
+d3-path@1:
+ version "1.0.9"
+ resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf"
+ integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==
+
+d3-scale-chromatic@^1.1.1:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz#54e333fc78212f439b14641fb55801dd81135a98"
+ integrity sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==
+ dependencies:
+ d3-color "1"
+ d3-interpolate "1"
+
+d3-scale@^1.0.6:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-1.0.7.tgz#fa90324b3ea8a776422bd0472afab0b252a0945d"
+ integrity sha512-KvU92czp2/qse5tUfGms6Kjig0AhHOwkzXG0+PqIJB3ke0WUv088AHMZI0OssO9NCkXt4RP8yju9rpH8aGB7Lw==
+ dependencies:
+ d3-array "^1.2.0"
+ d3-collection "1"
+ d3-color "1"
+ d3-format "1"
+ d3-interpolate "1"
+ d3-time "1"
+ d3-time-format "2"
+
+d3-selection-multi@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/d3-selection-multi/-/d3-selection-multi-1.0.1.tgz#cd6c25413d04a2cb97470e786f2cd877f3e34f58"
+ integrity sha1-zWwlQT0EosuXRw54byzYd/PjT1g=
+ dependencies:
+ d3-selection "1"
+ d3-transition "1"
+
+d3-selection@1, d3-selection@^1.1.0:
+ version "1.4.2"
+ resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.4.2.tgz#dcaa49522c0dbf32d6c1858afc26b6094555bc5c"
+ integrity sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==
+
+d3-shape@^1.2.0:
+ version "1.3.7"
+ resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7"
+ integrity sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==
+ dependencies:
+ d3-path "1"
+
+d3-time-format@2, d3-time-format@^2.0.5:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.3.0.tgz#107bdc028667788a8924ba040faf1fbccd5a7850"
+ integrity sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==
+ dependencies:
+ d3-time "1"
+
+d3-time@1, d3-time@^1.0.7:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.1.0.tgz#b1e19d307dae9c900b7e5b25ffc5dcc249a8a0f1"
+ integrity sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==
+
+d3-timer@1:
+ version "1.0.10"
+ resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.10.tgz#dfe76b8a91748831b13b6d9c793ffbd508dd9de5"
+ integrity sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==
+
+d3-transition@1, d3-transition@^1.1.0:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.3.2.tgz#a98ef2151be8d8600543434c1ca80140ae23b398"
+ integrity sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA==
+ dependencies:
+ d3-color "1"
+ d3-dispatch "1"
+ d3-ease "1"
+ d3-interpolate "1"
+ d3-selection "^1.1.0"
+ d3-timer "1"
+
d@1, d@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a"
@@ -4031,6 +4183,11 @@ dom-helpers@^5.0.1, dom-helpers@^5.1.0, dom-helpers@^5.1.2:
"@babel/runtime" "^7.8.7"
csstype "^3.0.2"
+dom-resize@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/dom-resize/-/dom-resize-1.0.3.tgz#df9d71e808171fdb66ee88517b0d1c02cdb98876"
+ integrity sha512-lohasnGy9LABj1Sq7ZPUGIWSYf+4LFUwL0Aev+dAzxSzUiovc+lKnFyrc6M2TycMIoH7674kwKaucRIPZPgJXw==
+
dom-serializer@0:
version "0.2.2"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
@@ -4039,6 +4196,11 @@ dom-serializer@0:
domelementtype "^2.0.1"
entities "^2.0.0"
+dom-walk@^0.1.0:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.2.tgz#0c548bef048f4d1f2a97249002236060daa3fd84"
+ integrity sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==
+
domain-browser@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
@@ -4068,6 +4230,11 @@ domhandler@^2.3.0:
dependencies:
domelementtype "1"
+dompurify@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.1.1.tgz#b5aa988676b093a9c836d8b855680a8598af25fe"
+ integrity sha512-NijiNVkS/OL8mdQL1hUbCD6uty/cgFpmNiuFxrmJ5YPH2cXrPKIewoixoji56rbZ6XBPmtM8GA8/sf9unlSuwg==
+
domutils@1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
@@ -4180,6 +4347,13 @@ encodeurl@~1.0.2:
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
+encoding@^0.1.11:
+ version "0.1.13"
+ resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9"
+ integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==
+ dependencies:
+ iconv-lite "^0.6.2"
+
end-of-stream@^1.0.0, end-of-stream@^1.1.0:
version "1.4.4"
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
@@ -4221,23 +4395,23 @@ error-ex@^1.2.0, error-ex@^1.3.1:
is-arrayish "^0.2.1"
es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.5:
- version "1.17.6"
- resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a"
- integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==
+ version "1.17.7"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c"
+ integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==
dependencies:
es-to-primitive "^1.2.1"
function-bind "^1.1.1"
has "^1.0.3"
has-symbols "^1.0.1"
- is-callable "^1.2.0"
- is-regex "^1.1.0"
- object-inspect "^1.7.0"
+ is-callable "^1.2.2"
+ is-regex "^1.1.1"
+ object-inspect "^1.8.0"
object-keys "^1.1.1"
- object.assign "^4.1.0"
+ object.assign "^4.1.1"
string.prototype.trimend "^1.0.1"
string.prototype.trimstart "^1.0.1"
-es-abstract@^1.18.0-next.0:
+es-abstract@^1.18.0-next.0, es-abstract@^1.18.0-next.1:
version "1.18.0-next.1"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.1.tgz#6e3a0a4bda717e5023ab3b8e90bec36108d22c68"
integrity sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==
@@ -4736,7 +4910,7 @@ fast-json-stable-stringify@^2.0.0:
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
-fast-levenshtein@~2.0.6:
+fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
@@ -4762,6 +4936,26 @@ fb-watchman@^2.0.0:
dependencies:
bser "2.1.1"
+fbemitter@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/fbemitter/-/fbemitter-2.1.1.tgz#523e14fdaf5248805bb02f62efc33be703f51865"
+ integrity sha1-Uj4U/a9SSIBbsC9i78M75wP1GGU=
+ dependencies:
+ fbjs "^0.8.4"
+
+fbjs@^0.8.0, fbjs@^0.8.4:
+ version "0.8.17"
+ resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd"
+ integrity sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=
+ dependencies:
+ core-js "^1.0.0"
+ isomorphic-fetch "^2.1.1"
+ loose-envify "^1.0.0"
+ object-assign "^4.1.0"
+ promise "^7.1.1"
+ setimmediate "^1.0.5"
+ ua-parser-js "^0.7.18"
+
figgy-pudding@^3.5.1:
version "3.5.2"
resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e"
@@ -4913,6 +5107,14 @@ flush-write-stream@^1.0.0:
inherits "^2.0.3"
readable-stream "^2.3.6"
+flux@^3.1.3:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/flux/-/flux-3.1.3.tgz#d23bed515a79a22d933ab53ab4ada19d05b2f08a"
+ integrity sha1-0jvtUVp5oi2TOrU6tK2hnQWy8Io=
+ dependencies:
+ fbemitter "^2.0.0"
+ fbjs "^0.8.0"
+
follow-redirects@^1.0.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db"
@@ -5188,6 +5390,14 @@ global-prefix@^3.0.0:
kind-of "^6.0.2"
which "^1.3.1"
+global@^4.3.0:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/global/-/global-4.4.0.tgz#3e7b105179006a323ed71aafca3e9c57a5cc6406"
+ integrity sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==
+ dependencies:
+ min-document "^2.19.0"
+ process "^0.11.10"
+
globals@^11.1.0:
version "11.12.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
@@ -5387,6 +5597,11 @@ hmac-drbg@^1.0.0:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
+hoist-non-react-statics@^2.5.0:
+ version "2.5.5"
+ resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
+ integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==
+
hoist-non-react-statics@^3.1.0:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
@@ -5530,7 +5745,18 @@ http-proxy-middleware@0.19.1:
lodash "^4.17.11"
micromatch "^3.1.10"
-http-proxy@^1.17.0:
+http-proxy-middleware@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-1.0.5.tgz#4c6e25d95a411e3d750bc79ccf66290675176dc2"
+ integrity sha512-CKzML7u4RdGob8wuKI//H8Ein6wNTEQR7yjVEzPbhBLGdOfkfvgTnp2HLnniKBDP9QW4eG10/724iTWLBeER3g==
+ dependencies:
+ "@types/http-proxy" "^1.17.4"
+ http-proxy "^1.18.1"
+ is-glob "^4.0.1"
+ lodash "^4.17.19"
+ micromatch "^4.0.2"
+
+http-proxy@^1.17.0, http-proxy@^1.18.1:
version "1.18.1"
resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549"
integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==
@@ -5560,6 +5786,13 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24:
dependencies:
safer-buffer ">= 2.1.2 < 3"
+iconv-lite@^0.6.2:
+ version "0.6.2"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.2.tgz#ce13d1875b0c3a674bd6a04b7f76b01b1b6ded01"
+ integrity sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3.0.0"
+
icss-utils@^4.0.0, icss-utils@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467"
@@ -5599,6 +5832,16 @@ immer@1.10.0:
resolved "https://registry.yarnpkg.com/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d"
integrity sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg==
+immutable-devtools@0.0.4:
+ version "0.0.4"
+ resolved "https://registry.yarnpkg.com/immutable-devtools/-/immutable-devtools-0.0.4.tgz#1e7e87f2c7a4f0533955bc4c2922d124bf9129dd"
+ integrity sha1-Hn6H8sek8FM5VbxMKSLRJL+RKd0=
+
+immutable@^3.6.4:
+ version "3.8.2"
+ resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3"
+ integrity sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=
+
import-cwd@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9"
@@ -5752,7 +5995,7 @@ internal-slot@^1.0.2:
has "^1.0.3"
side-channel "^1.0.2"
-invariant@^2.2.2, invariant@^2.2.4:
+invariant@^2.1.1, invariant@^2.2.2, invariant@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
@@ -5837,7 +6080,7 @@ is-buffer@^1.0.2, is-buffer@^1.1.5:
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
-is-callable@^1.1.4, is-callable@^1.2.0, is-callable@^1.2.2:
+is-callable@^1.1.4, is-callable@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9"
integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==
@@ -6024,7 +6267,7 @@ is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4:
dependencies:
isobject "^3.0.1"
-is-regex@^1.0.4, is-regex@^1.1.0, is-regex@^1.1.1:
+is-regex@^1.0.4, is-regex@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9"
integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==
@@ -6046,7 +6289,7 @@ is-root@2.1.0:
resolved "https://registry.yarnpkg.com/is-root/-/is-root-2.1.0.tgz#809e18129cf1129644302a4f8544035d51984a9c"
integrity sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==
-is-stream@^1.1.0:
+is-stream@^1.0.1, is-stream@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
@@ -6124,6 +6367,14 @@ isobject@^3.0.0, isobject@^3.0.1:
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
+isomorphic-fetch@^2.1.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9"
+ integrity sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=
+ dependencies:
+ node-fetch "^1.0.1"
+ whatwg-fetch ">=0.10.0"
+
isstream@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
@@ -6940,6 +7191,16 @@ lodash._reinterpolate@^3.0.0:
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=
+lodash.curry@^4.0.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/lodash.curry/-/lodash.curry-4.1.1.tgz#248e36072ede906501d75966200a86dab8b23170"
+ integrity sha1-JI42By7ekGUB11lmIAqG2riyMXA=
+
+lodash.flow@^3.3.0:
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/lodash.flow/-/lodash.flow-3.5.0.tgz#87bf40292b8cf83e4e8ce1a3ae4209e20071675a"
+ integrity sha1-h79AKSuM+D5OjOGjrkIJ4gBxZ1o=
+
lodash.memoize@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
@@ -7157,6 +7418,11 @@ merge2@^1.2.3:
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
+merge@^1.2.0:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145"
+ integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==
+
methods@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
@@ -7186,6 +7452,14 @@ micromatch@^3.1.10, micromatch@^3.1.4:
snapdragon "^0.8.1"
to-regex "^3.0.2"
+micromatch@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259"
+ integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==
+ dependencies:
+ braces "^3.0.1"
+ picomatch "^2.0.5"
+
miller-rabin@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d"
@@ -7226,6 +7500,13 @@ mimic-fn@^2.0.0, mimic-fn@^2.1.0:
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
+min-document@^2.19.0:
+ version "2.19.0"
+ resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685"
+ integrity sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=
+ dependencies:
+ dom-walk "^0.1.0"
+
min-indent@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
@@ -7338,6 +7619,16 @@ mixin-object@^2.0.1:
dependencies:
minimist "^1.2.5"
+moment-duration-format@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/moment-duration-format/-/moment-duration-format-1.3.0.tgz#541771b5f87a049cc65540475d3ad966737d6908"
+ integrity sha1-VBdxtfh6BJzGVUBHXTrZZnN9aQg=
+
+moment@^2.18.1, moment@^2.24.0:
+ version "2.29.0"
+ resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.0.tgz#fcbef955844d91deb55438613ddcec56e86a3425"
+ integrity sha512-z6IJ5HXYiuxvFTI6eiQ9dm77uE0gyy1yXNApVHqTcnIKfY9tIwEjlzsZ6u1LQXvVgKeTnv9Xm7NDvJ7lso3MtA==
+
move-concurrently@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
@@ -7438,6 +7729,14 @@ no-case@^3.0.3:
lower-case "^2.0.1"
tslib "^1.10.0"
+node-fetch@^1.0.1:
+ version "1.7.3"
+ resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
+ integrity sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==
+ dependencies:
+ encoding "^0.1.11"
+ is-stream "^1.0.1"
+
node-forge@^0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3"
@@ -7651,18 +7950,18 @@ object-hash@^2.0.1:
resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.0.3.tgz#d12db044e03cd2ca3d77c0570d87225b02e1e6ea"
integrity sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg==
-object-inspect@^1.7.0, object-inspect@^1.8.0:
+object-inspect@^1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0"
integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==
object-is@^1.0.1:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.2.tgz#c5d2e87ff9e119f78b7a088441519e2eec1573b6"
- integrity sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ==
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.3.tgz#2e3b9e65560137455ee3bd62aec4d90a2ea1cc81"
+ integrity sha512-teyqLvFWzLkq5B9ki8FVWA902UER2qkxmdA4nLf+wjOLAWgxzCWZNCxpDq9MvE8MmhWNr+I8w3BN49Vx36Y6Xg==
dependencies:
define-properties "^1.1.3"
- es-abstract "^1.17.5"
+ es-abstract "^1.18.0-next.1"
object-keys@^1.0.12, object-keys@^1.1.1:
version "1.1.1"
@@ -8132,7 +8431,7 @@ performance-now@^2.1.0:
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
-picomatch@^2.0.4, picomatch@^2.2.1:
+picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1:
version "2.2.2"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==
@@ -8218,6 +8517,17 @@ pnp-webpack-plugin@1.6.4:
dependencies:
ts-pnp "^1.1.6"
+pondjs@^0.9.0:
+ version "0.9.0"
+ resolved "https://registry.yarnpkg.com/pondjs/-/pondjs-0.9.0.tgz#5c931233f8ef56852914eb669506eaac62b0f13b"
+ integrity sha512-fXgEYrWhgC/N3CVuXarG+/q54jar35Mqf7QPdl7z+mX5RkMyyjLfj9fMgY3oHv2hzMo9zkNx2kK62DrZuG3LXg==
+ dependencies:
+ "@babel/polyfill" "^7.7.0"
+ immutable "^3.6.4"
+ immutable-devtools "0.0.4"
+ moment "^2.24.0"
+ underscore "^1.9.1"
+
portfinder@^1.0.25:
version "1.0.28"
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778"
@@ -8967,6 +9277,13 @@ promise-inflight@^1.0.1:
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
+promise@^7.1.1:
+ version "7.3.1"
+ resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
+ integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==
+ dependencies:
+ asap "~2.0.3"
+
promise@^8.0.3:
version "8.1.0"
resolved "https://registry.yarnpkg.com/promise/-/promise-8.1.0.tgz#697c25c3dfe7435dd79fcd58c38a135888eaf05e"
@@ -8990,7 +9307,7 @@ prop-types-extra@^1.1.0:
react-is "^16.3.2"
warning "^4.0.0"
-prop-types@^15.6.2, prop-types@^15.7.2:
+prop-types@^15.5.10, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@@ -9074,6 +9391,11 @@ punycode@^2.1.0, punycode@^2.1.1:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
+pure-color@^1.2.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/pure-color/-/pure-color-1.3.0.tgz#1fe064fb0ac851f0de61320a8bf796836422f33e"
+ integrity sha1-H+Bk+wrIUfDeYTIKi/eWg2Qi8z4=
+
q@^1.1.2:
version "1.5.1"
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
@@ -9161,6 +9483,16 @@ react-app-polyfill@^1.0.6:
regenerator-runtime "^0.13.3"
whatwg-fetch "^3.0.0"
+react-base16-styling@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/react-base16-styling/-/react-base16-styling-0.6.0.tgz#ef2156d66cf4139695c8a167886cb69ea660792c"
+ integrity sha1-7yFW1mz0E5aVyKFniGy2nqZgeSw=
+ dependencies:
+ base16 "^1.0.0"
+ lodash.curry "^4.0.1"
+ lodash.flow "^3.3.0"
+ pure-color "^1.2.0"
+
react-bootstrap@^1.0.1:
version "1.3.0"
resolved "https://registry.yarnpkg.com/react-bootstrap/-/react-bootstrap-1.3.0.tgz#d9dde4ad554e9cd21d1465e8b5e5ef6679cae6a1"
@@ -9230,6 +9562,18 @@ react-error-overlay@^6.0.7:
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.7.tgz#1dcfb459ab671d53f660a991513cb2f0a0553108"
integrity sha512-TAv1KJFh3RhqxNvhzxj6LeT5NWklP6rDr2a0jaTfsZ5wSZWHOGeqQyejUp3xxLfPt2UpyJEcVQB/zyPcmonNFA==
+react-hot-loader@4.1.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.1.2.tgz#5e8025f5bc5605506586b46eb2c6cc4006fd54d7"
+ integrity sha512-7EFwgpJOx4AG4pwVifgr/ZNBPAxl2z424nGJPc/APB3F8YtCA3WdYuGlcerRK2C9vYAoqiiLw745IB3wjnzrRQ==
+ dependencies:
+ fast-levenshtein "^2.0.6"
+ global "^4.3.0"
+ hoist-non-react-statics "^2.5.0"
+ prop-types "^15.6.1"
+ react-lifecycles-compat "^3.0.2"
+ shallowequal "^1.0.2"
+
react-input-mask@^3.0.0-alpha.2:
version "3.0.0-alpha.2"
resolved "https://registry.yarnpkg.com/react-input-mask/-/react-input-mask-3.0.0-alpha.2.tgz#113102942a557edc7a192e66020b8ce1ba699a5c"
@@ -9244,7 +9588,17 @@ react-is@^16.12.0, react-is@^16.3.2, react-is@^16.6.0, react-is@^16.7.0, react-i
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
-react-lifecycles-compat@^3.0.4:
+react-json-view@^1.19.1:
+ version "1.19.1"
+ resolved "https://registry.yarnpkg.com/react-json-view/-/react-json-view-1.19.1.tgz#95d8e59e024f08a25e5dc8f076ae304eed97cf5c"
+ integrity sha512-u5e0XDLIs9Rj43vWkKvwL8G3JzvXSl6etuS5G42a8klMohZuYFQzSN6ri+/GiBptDqlrXPTdExJVU7x9rrlXhg==
+ dependencies:
+ flux "^3.1.3"
+ react-base16-styling "^0.6.0"
+ react-lifecycles-compat "^3.0.4"
+ react-textarea-autosize "^6.1.0"
+
+react-lifecycles-compat@^3.0.2, react-lifecycles-compat@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
@@ -9357,6 +9711,42 @@ react-tag-autocomplete@^6.0.0-beta.6:
resolved "https://registry.yarnpkg.com/react-tag-autocomplete/-/react-tag-autocomplete-6.1.0.tgz#9fb70149a69b33379013e5255bcd7ad97d8ec06b"
integrity sha512-AMhVqxEEIrOfzH0A9XrpsTaLZCVYgjjxp3DSTuSvx91LBSFI6uYcKe38ltR/H/TQw4aytofVghQ1hR9sKpXRQA==
+react-textarea-autosize@^6.1.0:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-6.1.0.tgz#df91387f8a8f22020b77e3833c09829d706a09a5"
+ integrity sha512-F6bI1dgib6fSvG8so1HuArPUv+iVEfPliuLWusLF+gAKz0FbB4jLrWUrTAeq1afnPT2c9toEZYUdz/y1uKMy4A==
+ dependencies:
+ prop-types "^15.6.0"
+
+react-timeseries-charts@^0.16.1:
+ version "0.16.1"
+ resolved "https://registry.yarnpkg.com/react-timeseries-charts/-/react-timeseries-charts-0.16.1.tgz#46675a41a7806155a0b5a1342e3b811172845a5c"
+ integrity sha512-WK2Pege5/FGrE5ifGX1XTVQOZkobRUV5YMwRQi4+TpYAALL5cBLp+XCuAX8x8agq4AmqChpClPIrANc8pRL5RA==
+ dependencies:
+ array.prototype.fill "^1.0.1"
+ babel-runtime "^6.23.0"
+ colorbrewer "^1.0.0"
+ d3-axis "^1.0.8"
+ d3-ease "^1.0.3"
+ d3-format "^1.2.0"
+ d3-interpolate "^1.1.5"
+ d3-scale "^1.0.6"
+ d3-scale-chromatic "^1.1.1"
+ d3-selection "^1.1.0"
+ d3-selection-multi "^1.0.1"
+ d3-shape "^1.2.0"
+ d3-time "^1.0.7"
+ d3-time-format "^2.0.5"
+ d3-transition "^1.1.0"
+ dom-resize "^1.0.3"
+ invariant "^2.1.1"
+ merge "^1.2.0"
+ moment "^2.18.1"
+ moment-duration-format "^1.3.0"
+ prop-types "^15.5.10"
+ react-hot-loader "4.1.2"
+ underscore "^1.8.3"
+
react-transition-group@^4.4.1:
version "4.4.1"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9"
@@ -9845,7 +10235,7 @@ safe-regex@^1.1.0:
dependencies:
ret "~0.1.10"
-"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
+"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
@@ -10043,7 +10433,7 @@ set-value@^2.0.0, set-value@^2.0.1:
is-plain-object "^2.0.3"
split-string "^3.0.1"
-setimmediate@^1.0.4:
+setimmediate@^1.0.4, setimmediate@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
@@ -10083,6 +10473,11 @@ shallow-clone@^3.0.0:
dependencies:
kind-of "^6.0.2"
+shallowequal@^1.0.2:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8"
+ integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==
+
shebang-command@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
@@ -10971,6 +11366,11 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
+ua-parser-js@^0.7.18:
+ version "0.7.22"
+ resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.22.tgz#960df60a5f911ea8f1c818f3747b99c6e177eae3"
+ integrity sha512-YUxzMjJ5T71w6a8WWVcMGM6YWOTX27rCoIQgLXiWaxqXSx9D7DNjiGWn1aJIRSQ5qr0xuhra77bSIh6voR/46Q==
+
uncontrollable@^7.0.0:
version "7.1.1"
resolved "https://registry.yarnpkg.com/uncontrollable/-/uncontrollable-7.1.1.tgz#f67fed3ef93637126571809746323a9db815d556"
@@ -10981,6 +11381,11 @@ uncontrollable@^7.0.0:
invariant "^2.2.4"
react-lifecycles-compat "^3.0.4"
+underscore@^1.8.3, underscore@^1.9.1:
+ version "1.11.0"
+ resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.11.0.tgz#dd7c23a195db34267186044649870ff1bab5929e"
+ integrity sha512-xY96SsN3NA461qIRKZ/+qox37YXPtSBswMGfiNptr+wrt6ds4HaMw23TP612fEyGekRE6LNRiLYr/aqbHXNedw==
+
unicode-canonical-property-names-ecmascript@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"
@@ -11393,7 +11798,7 @@ whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3, whatwg-encoding@^1.0.5:
dependencies:
iconv-lite "0.4.24"
-whatwg-fetch@^3.0.0:
+whatwg-fetch@>=0.10.0, whatwg-fetch@^3.0.0:
version "3.4.1"
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.4.1.tgz#e5f871572d6879663fa5674c8f833f15a8425ab3"
integrity sha512-sofZVzE1wKwO+EYPbWfiwzaKovWiZXf4coEzjGP9b2GBVgQRLQUZ2QcuPpQExGDAW5GItpEm6Tl4OU5mywnAoQ==
diff --git a/go.mod b/go.mod
index 308b16b..7834dbe 100644
--- a/go.mod
+++ b/go.mod
@@ -3,14 +3,18 @@ module github.com/eciavatta/caronte
go 1.14
require (
+ github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect
github.com/flier/gohs v1.0.0
github.com/gin-gonic/contrib v0.0.0-20191209060500-d6e26eeaa607
github.com/gin-gonic/gin v1.6.2
+ github.com/go-ole/go-ole v1.2.4 // indirect
github.com/go-playground/validator/v10 v10.2.0
github.com/golang/protobuf v1.3.5 // indirect
github.com/google/gopacket v1.1.17
+ github.com/gorilla/websocket v1.4.2
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
+ github.com/shirou/gopsutil v2.20.9+incompatible
github.com/sirupsen/logrus v1.4.2
github.com/smartystreets/assertions v1.0.0 // indirect
github.com/stretchr/testify v1.4.0
diff --git a/go.sum b/go.sum
index fd63c39..b507337 100644
--- a/go.sum
+++ b/go.sum
@@ -1,4 +1,6 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=
+github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -10,6 +12,8 @@ github.com/gin-gonic/contrib v0.0.0-20191209060500-d6e26eeaa607 h1:MrIm8EEPue08J
github.com/gin-gonic/contrib v0.0.0-20191209060500-d6e26eeaa607/go.mod h1:iqneQ2Df3omzIVTkIfn7c1acsVnMGiSLn4XF5Blh3Yg=
github.com/gin-gonic/gin v1.6.2 h1:88crIK23zO6TqlQBt+f9FrPJNKm9ZEr7qjp9vl/d5TM=
github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
+github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
+github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
@@ -52,12 +56,15 @@ github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gopacket v1.1.14/go.mod h1:UCLx9mCmAwsVbn6qQl1WIEt2SO7Nd2fD0th1TBAsqBw=
github.com/google/gopacket v1.1.17 h1:rMrlX2ZY2UbvT+sdz3+6J+pp2z+msCq9MxTU6ymxbBY=
github.com/google/gopacket v1.1.17/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM=
github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c h1:16eHWuMGvCjSfgRJKqIzapE78onvvTbdi1rMkU00lZw=
github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
+github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
@@ -102,9 +109,13 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULUx4=
+github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/shirou/gopsutil v2.20.9+incompatible h1:msXs2frUV+O/JLva9EDLpuJ84PrFsdCTCQex8PUdtkQ=
+github.com/shirou/gopsutil v2.20.9+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
@@ -150,6 +161,8 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -159,6 +172,7 @@ golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2 h1:T5DasATyLQfmbTpfEXx/IOL9vfjzW6up+ZDkmHvIf2s=
golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200406155108-e3b113bbe6a4 h1:c1Sgqkh8v6ZxafNGG64r8C8UisIW2TKMJN8P86tKjr0=
diff --git a/notification_controller.go b/notification_controller.go
new file mode 100644
index 0000000..b9b3b1c
--- /dev/null
+++ b/notification_controller.go
@@ -0,0 +1,178 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package main
+
+import (
+ "github.com/gin-gonic/gin"
+ "github.com/gorilla/websocket"
+ log "github.com/sirupsen/logrus"
+ "net"
+ "net/http"
+ "time"
+)
+
+const (
+ writeWait = 10 * time.Second
+ pongWait = 60 * time.Second
+ pingPeriod = (pongWait * 9) / 10
+ maxMessageSize = 512
+)
+
+type NotificationController struct {
+ upgrader websocket.Upgrader
+ clients map[net.Addr]*client
+ broadcast chan interface{}
+ register chan *client
+ unregister chan *client
+ applicationContext *ApplicationContext
+}
+
+func NewNotificationController(applicationContext *ApplicationContext) *NotificationController {
+ return &NotificationController{
+ upgrader: websocket.Upgrader{
+ ReadBufferSize: 1024,
+ WriteBufferSize: 1024,
+ },
+ clients: make(map[net.Addr]*client),
+ broadcast: make(chan interface{}),
+ register: make(chan *client),
+ unregister: make(chan *client),
+ applicationContext: applicationContext,
+ }
+}
+
+type client struct {
+ conn *websocket.Conn
+ send chan interface{}
+ notificationController *NotificationController
+}
+
+func (wc *NotificationController) NotificationHandler(w http.ResponseWriter, r *http.Request) error {
+ conn, err := wc.upgrader.Upgrade(w, r, nil)
+ if err != nil {
+ log.WithError(err).Error("failed to set websocket upgrade")
+ return err
+ }
+
+ client := &client{
+ conn: conn,
+ send: make(chan interface{}),
+ notificationController: wc,
+ }
+ wc.register <- client
+ go client.readPump()
+ go client.writePump()
+
+ return nil
+}
+
+func (wc *NotificationController) Run() {
+ for {
+ select {
+ case client := <-wc.register:
+ wc.clients[client.conn.RemoteAddr()] = client
+ payload := gin.H{"event": "connected", "message": gin.H{
+ "version": wc.applicationContext.Version,
+ "is_configured": wc.applicationContext.IsConfigured,
+ "connected_clients": len(wc.clients),
+ }}
+ client.send <- payload
+ log.WithField("connected_clients", len(wc.clients)).
+ WithField("remote_address", client.conn.RemoteAddr()).
+ Info("[+] a websocket client connected")
+ case client := <-wc.unregister:
+ if _, ok := wc.clients[client.conn.RemoteAddr()]; ok {
+ close(client.send)
+ _ = client.conn.WriteMessage(websocket.CloseMessage, nil)
+ _ = client.conn.Close()
+ delete(wc.clients, client.conn.RemoteAddr())
+ log.WithField("connected_clients", len(wc.clients)).
+ WithField("remote_address", client.conn.RemoteAddr()).
+ Info("[-] a websocket client disconnected")
+ }
+ case payload := <-wc.broadcast:
+ for _, client := range wc.clients {
+ select {
+ case client.send <- payload:
+ default:
+ close(client.send)
+ delete(wc.clients, client.conn.RemoteAddr())
+ }
+ }
+ }
+ }
+}
+
+func (wc *NotificationController) Notify(event string, message interface{}) {
+ wc.broadcast <- gin.H{"event": event, "message": message}
+}
+
+func (c *client) readPump() {
+ c.conn.SetReadLimit(maxMessageSize)
+ if err := c.conn.SetReadDeadline(time.Now().Add(pongWait)); err != nil {
+ c.close()
+ return
+ }
+ c.conn.SetPongHandler(func(string) error { return c.conn.SetReadDeadline(time.Now().Add(pongWait)) })
+ for {
+ if _, _, err := c.conn.ReadMessage(); err != nil {
+ if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
+ log.WithError(err).WithField("remote_address", c.conn.RemoteAddr()).
+ Warn("unexpected websocket disconnection")
+ }
+ break
+ }
+ }
+
+ c.close()
+}
+
+func (c *client) writePump() {
+ ticker := time.NewTicker(pingPeriod)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case payload, ok := <-c.send:
+ if !ok {
+ return
+ }
+ if err := c.conn.SetWriteDeadline(time.Now().Add(writeWait)); err != nil {
+ c.close()
+ return
+ }
+ if err := c.conn.WriteJSON(payload); err != nil {
+ c.close()
+ return
+ }
+ case <-ticker.C:
+ if err := c.conn.SetWriteDeadline(time.Now().Add(writeWait)); err != nil {
+ c.close()
+ return
+ }
+ if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
+ c.close()
+ return
+ }
+ }
+ }
+}
+
+func (c *client) close() {
+ c.notificationController.unregister <- c
+}
diff --git a/parsers/http_request_parser.go b/parsers/http_request_parser.go
index e2224b8..98ba8e3 100644
--- a/parsers/http_request_parser.go
+++ b/parsers/http_request_parser.go
@@ -1,3 +1,20 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
package parsers
import (
@@ -11,7 +28,7 @@ import (
"strings"
)
-type HttpRequestMetadata struct {
+type HTTPRequestMetadata struct {
BasicMetadata
Method string `json:"method"`
URL string `json:"url"`
@@ -23,19 +40,19 @@ type HttpRequestMetadata struct {
FormData map[string]string `json:"form_data" binding:"omitempty"`
Body string `json:"body" binding:"omitempty"`
Trailer map[string]string `json:"trailer" binding:"omitempty"`
- Reproducers HttpRequestMetadataReproducers `json:"reproducers"`
+ Reproducers HTTPRequestMetadataReproducers `json:"reproducers"`
}
-type HttpRequestMetadataReproducers struct {
+type HTTPRequestMetadataReproducers struct {
CurlCommand string `json:"curl_command"`
RequestsCode string `json:"requests_code"`
FetchRequest string `json:"fetch_request"`
}
-type HttpRequestParser struct {
+type HTTPRequestParser struct {
}
-func (p HttpRequestParser) TryParse(content []byte) Metadata {
+func (p HTTPRequestParser) TryParse(content []byte) Metadata {
reader := bufio.NewReader(bytes.NewReader(content))
request, err := http.ReadRequest(reader)
if err != nil {
@@ -51,7 +68,7 @@ func (p HttpRequestParser) TryParse(content []byte) Metadata {
_ = request.Body.Close()
_ = request.ParseForm()
- return HttpRequestMetadata{
+ return HTTPRequestMetadata{
BasicMetadata: BasicMetadata{"http-request"},
Method: request.Method,
URL: request.URL.String(),
@@ -63,9 +80,9 @@ func (p HttpRequestParser) TryParse(content []byte) Metadata {
FormData: JoinArrayMap(request.Form),
Body: body,
Trailer: JoinArrayMap(request.Trailer),
- Reproducers: HttpRequestMetadataReproducers{
+ Reproducers: HTTPRequestMetadataReproducers{
CurlCommand: curlCommand(content),
- RequestsCode: requestsCode(request),
+ RequestsCode: requestsCode(request, body),
FetchRequest: fetchRequest(request, body),
},
}
@@ -75,26 +92,22 @@ 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 {
+ command, err := http2curl.GetCurlCommand(request)
+ if err == nil {
return command.String()
- } else {
- return err.Error()
}
+ return err.Error()
}
-func requestsCode(request *http.Request) string {
+func requestsCode(request *http.Request, body string) string {
var b strings.Builder
- var params string
- if request.Form != nil {
- params = toJson(JoinArrayMap(request.PostForm))
- }
- headers := toJson(JoinArrayMap(request.Header))
- cookies := toJson(CookiesMap(request.Cookies()))
+ headers := toJSON(JoinArrayMap(request.Header))
+ cookies := toJSON(CookiesMap(request.Cookies()))
b.WriteString("import requests\n\nresponse = requests." + strings.ToLower(request.Method) + "(")
b.WriteString("\"" + request.URL.String() + "\"")
- if params != "" {
- b.WriteString(", data = " + params)
+ if body != "" {
+ b.WriteString(", data = \"" + strings.Replace(body, "\"", "\\\"", -1) + "\"")
}
if headers != "" {
b.WriteString(", headers = " + headers)
@@ -133,14 +146,13 @@ func fetchRequest(request *http.Request, body string) string {
data["method"] = request.Method
// TODO: mode
- if jsonData := toJson(data); jsonData != "" {
+ if jsonData := toJSON(data); jsonData != "" {
return "fetch(\"" + request.URL.String() + "\", " + jsonData + ");"
- } else {
- return "invalid-request"
}
+ return "invalid-request"
}
-func toJson(obj interface{}) string {
+func toJSON(obj interface{}) string {
if buffer, err := json.MarshalIndent(obj, "", "\t"); err == nil {
return string(buffer)
} else {
diff --git a/parsers/http_response_parser.go b/parsers/http_response_parser.go
index 1770116..e61fffd 100644
--- a/parsers/http_response_parser.go
+++ b/parsers/http_response_parser.go
@@ -1,3 +1,20 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
package parsers
import (
@@ -9,7 +26,7 @@ import (
"net/http"
)
-type HttpResponseMetadata struct {
+type HTTPResponseMetadata struct {
BasicMetadata
Status string `json:"status"`
StatusCode int `json:"status_code"`
@@ -23,10 +40,10 @@ type HttpResponseMetadata struct {
Trailer map[string]string `json:"trailer" binding:"omitempty"`
}
-type HttpResponseParser struct {
+type HTTPResponseParser struct {
}
-func (p HttpResponseParser) TryParse(content []byte) Metadata {
+func (p HTTPResponseParser) TryParse(content []byte) Metadata {
reader := bufio.NewReader(bytes.NewReader(content))
response, err := http.ReadResponse(reader, nil)
if err != nil {
@@ -57,11 +74,11 @@ func (p HttpResponseParser) TryParse(content []byte) Metadata {
_ = response.Body.Close()
var location string
- if locationUrl, err := response.Location(); err == nil {
- location = locationUrl.String()
+ if locationURL, err := response.Location(); err == nil {
+ location = locationURL.String()
}
- return HttpResponseMetadata{
+ return HTTPResponseMetadata{
BasicMetadata: BasicMetadata{"http-response"},
Status: response.Status,
StatusCode: response.StatusCode,
diff --git a/parsers/parser.go b/parsers/parser.go
index 06cc0dc..9aca3b6 100644
--- a/parsers/parser.go
+++ b/parsers/parser.go
@@ -1,3 +1,20 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
package parsers
type Parser interface {
@@ -13,8 +30,8 @@ type BasicMetadata struct {
}
var parsers = []Parser{ // order matter
- HttpRequestParser{},
- HttpResponseParser{},
+ HTTPRequestParser{},
+ HTTPResponseParser{},
}
func Parse(content []byte) Metadata {
diff --git a/parsers/parser_utils.go b/parsers/parser_utils.go
index b688262..575b666 100644
--- a/parsers/parser_utils.go
+++ b/parsers/parser_utils.go
@@ -1,3 +1,20 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
package parsers
import (
diff --git a/pcap_importer.go b/pcap_importer.go
index 1739b3f..1c80b3f 100644
--- a/pcap_importer.go
+++ b/pcap_importer.go
@@ -1,3 +1,20 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
package main
import (
@@ -12,6 +29,7 @@ import (
"os"
"path"
"path/filepath"
+ "sort"
"sync"
"time"
)
@@ -19,17 +37,17 @@ import (
const PcapsBasePath = "pcaps/"
const ProcessingPcapsBasePath = PcapsBasePath + "processing/"
const initialAssemblerPoolSize = 16
-const flushOlderThan = 5 * time.Minute
const importUpdateProgressInterval = 100 * time.Millisecond
type PcapImporter struct {
- storage Storage
- streamPool *tcpassembly.StreamPool
- assemblers []*tcpassembly.Assembler
- sessions map[string]ImportingSession
- mAssemblers sync.Mutex
- mSessions sync.Mutex
- serverNet net.IPNet
+ storage Storage
+ streamPool *tcpassembly.StreamPool
+ assemblers []*tcpassembly.Assembler
+ sessions map[string]ImportingSession
+ mAssemblers sync.Mutex
+ mSessions sync.Mutex
+ serverNet net.IPNet
+ notificationController *NotificationController
}
type ImportingSession struct {
@@ -47,7 +65,8 @@ type ImportingSession struct {
type flowCount [2]int
-func NewPcapImporter(storage Storage, serverNet net.IPNet, rulesManager RulesManager) *PcapImporter {
+func NewPcapImporter(storage Storage, serverNet net.IPNet, rulesManager RulesManager,
+ notificationController *NotificationController) *PcapImporter {
streamPool := tcpassembly.NewStreamPool(NewBiDirectionalStreamFactory(storage, serverNet, rulesManager))
var result []ImportingSession
@@ -60,13 +79,14 @@ func NewPcapImporter(storage Storage, serverNet net.IPNet, rulesManager RulesMan
}
return &PcapImporter{
- storage: storage,
- streamPool: streamPool,
- assemblers: make([]*tcpassembly.Assembler, 0, initialAssemblerPoolSize),
- sessions: sessions,
- mAssemblers: sync.Mutex{},
- mSessions: sync.Mutex{},
- serverNet: serverNet,
+ storage: storage,
+ streamPool: streamPool,
+ assemblers: make([]*tcpassembly.Assembler, 0, initialAssemblerPoolSize),
+ sessions: sessions,
+ mAssemblers: sync.Mutex{},
+ mSessions: sync.Mutex{},
+ serverNet: serverNet,
+ notificationController: notificationController,
}
}
@@ -120,6 +140,9 @@ func (pi *PcapImporter) GetSessions() []ImportingSession {
for _, session := range pi.sessions {
sessions = append(sessions, session)
}
+ sort.Slice(sessions, func(i, j int) bool {
+ return sessions[i].StartedAt.Before(sessions[j].StartedAt)
+ })
pi.mSessions.Unlock()
return sessions
}
@@ -186,6 +209,8 @@ func (pi *PcapImporter) parsePcap(session ImportingSession, fileName string, flu
handle.Close()
pi.releaseAssembler(assembler)
pi.progressUpdate(session, fileName, true, "")
+ pi.notificationController.Notify("pcap.completed", session)
+
return
}
@@ -201,8 +226,8 @@ func (pi *PcapImporter) parsePcap(session ImportingSession, fileName string, flu
var servicePort uint16
var index int
- isDstServer := pi.serverNet.Contains(packet.NetworkLayer().NetworkFlow().Dst().Raw())
- isSrcServer := pi.serverNet.Contains(packet.NetworkLayer().NetworkFlow().Src().Raw())
+ isDstServer := pi.serverNet.Contains(packet.NetworkLayer().NetworkFlow().Dst().Raw())
+ isSrcServer := pi.serverNet.Contains(packet.NetworkLayer().NetworkFlow().Src().Raw())
if isDstServer && !isSrcServer {
servicePort = uint16(tcp.DstPort)
index = 0
@@ -284,7 +309,7 @@ func deleteProcessingFile(fileName string) {
}
func moveProcessingFile(sessionID string, fileName string) {
- if err := os.Rename(ProcessingPcapsBasePath + fileName, PcapsBasePath + sessionID + path.Ext(fileName)); err != nil {
+ if err := os.Rename(ProcessingPcapsBasePath+fileName, PcapsBasePath+sessionID+path.Ext(fileName)); err != nil {
log.WithError(err).Error("failed to move processed file")
}
}
diff --git a/pcap_importer_test.go b/pcap_importer_test.go
index be09ea9..a47f2d9 100644
--- a/pcap_importer_test.go
+++ b/pcap_importer_test.go
@@ -1,3 +1,20 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
package main
import (
@@ -110,6 +127,7 @@ func newTestPcapImporter(wrapper *TestStorageWrapper, serverAddress string) *Pca
mAssemblers: sync.Mutex{},
mSessions: sync.Mutex{},
serverNet: *ParseIPNet(serverAddress),
+ notificationController: NewNotificationController(nil),
}
}
diff --git a/resources_controller.go b/resources_controller.go
new file mode 100644
index 0000000..0576e0f
--- /dev/null
+++ b/resources_controller.go
@@ -0,0 +1,108 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package main
+
+import (
+ "context"
+ "github.com/gin-gonic/gin"
+ "github.com/shirou/gopsutil/cpu"
+ "github.com/shirou/gopsutil/disk"
+ "github.com/shirou/gopsutil/mem"
+ log "github.com/sirupsen/logrus"
+ "sync"
+ "time"
+)
+
+const (
+ averageCPUPercentAlertThreshold = 90.0
+ averageCPUPercentAlertMinInterval = 120.0
+)
+
+type SystemStats struct {
+ VirtualMemory *mem.VirtualMemoryStat `json:"virtual_memory"`
+ CPUTimes []cpu.TimesStat `json:"cpu_times"`
+ CPUPercents []float64 `json:"cpu_percents"`
+ DiskUsage *disk.UsageStat `json:"disk_usage"`
+}
+
+type ResourcesController struct {
+ notificationController *NotificationController
+ lastCPUPercent []float64
+ mutex sync.Mutex
+}
+
+func NewResourcesController(notificationController *NotificationController) *ResourcesController {
+ return &ResourcesController{
+ notificationController: notificationController,
+ }
+}
+
+func (csc *ResourcesController) GetProcessStats(c context.Context) interface{} {
+ return nil
+}
+
+func (csc *ResourcesController) GetSystemStats(c context.Context) SystemStats {
+ virtualMemory, err := mem.VirtualMemoryWithContext(c)
+ if err != nil {
+ log.WithError(err).Panic("failed to retrieve virtual memory")
+ }
+ cpuTimes, err := cpu.TimesWithContext(c, true)
+ if err != nil {
+ log.WithError(err).Panic("failed to retrieve cpu times")
+ }
+ diskUsage, err := disk.UsageWithContext(c, "/")
+ if err != nil {
+ log.WithError(err).Panic("failed to retrieve disk usage")
+ }
+
+ defer csc.mutex.Unlock()
+ csc.mutex.Lock()
+
+ return SystemStats{
+ VirtualMemory: virtualMemory,
+ CPUTimes: cpuTimes,
+ DiskUsage: diskUsage,
+ CPUPercents: csc.lastCPUPercent,
+ }
+}
+
+func (csc *ResourcesController) Run() {
+ interval, _ := time.ParseDuration("3s")
+ var lastAlertTime time.Time
+
+ for {
+ cpuPercent, err := cpu.Percent(interval, true)
+ if err != nil {
+ log.WithError(err).Error("failed to retrieve cpu percent")
+ return
+ }
+
+ csc.mutex.Lock()
+ csc.lastCPUPercent = cpuPercent
+ csc.mutex.Unlock()
+
+ avg := Average(cpuPercent)
+ if avg > averageCPUPercentAlertThreshold && time.Now().Sub(lastAlertTime).Seconds() > averageCPUPercentAlertMinInterval {
+ csc.notificationController.Notify("resources.cpu_alert", gin.H{
+ "cpu_percent": cpuPercent,
+ })
+ log.WithField("cpu_percent", cpuPercent).Warn("cpu percent usage has exceeded the limit threshold")
+ lastAlertTime = time.Now()
+ }
+ }
+}
diff --git a/rules_manager.go b/rules_manager.go
index a5dc7ce..5d6cded 100644
--- a/rules_manager.go
+++ b/rules_manager.go
@@ -1,3 +1,20 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
package main
import (
@@ -7,6 +24,7 @@ import (
"github.com/flier/gohs/hyperscan"
"github.com/go-playground/validator/v10"
log "github.com/sirupsen/logrus"
+ "sort"
"sync"
"time"
)
@@ -104,16 +122,22 @@ func LoadRulesManager(storage Storage, flagRegex string) (RulesManager, error) {
// if there are no rules in database (e.g. first run), set flagRegex as first rule
if len(rulesManager.rules) == 0 {
- if _, err := rulesManager.AddRule(context.Background(), Rule{
- Name: "flag",
- Color: "#E53935",
- Notes: "Mark connections where the flag is stolen",
+ _, _ = rulesManager.AddRule(context.Background(), Rule{
+ Name: "flag_out",
+ Color: "#e53935",
+ Notes: "Mark connections where the flags are stolen",
Patterns: []Pattern{
- {Regex: flagRegex, Direction: DirectionToClient},
+ {Regex: flagRegex, Direction: DirectionToClient, Flags: RegexFlags{Utf8Mode: true}},
},
- }); err != nil {
- return nil, err
- }
+ })
+ _, _ = rulesManager.AddRule(context.Background(), Rule{
+ Name: "flag_in",
+ Color: "#43A047",
+ Notes: "Mark connections where the flags are placed",
+ Patterns: []Pattern{
+ {Regex: flagRegex, Direction: DirectionToServer, Flags: RegexFlags{Utf8Mode: true}},
+ },
+ })
} else {
if err := rulesManager.generateDatabase(rules[len(rules)-1].ID); err != nil {
return nil, err
@@ -190,6 +214,10 @@ func (rm *rulesManagerImpl) GetRules() []Rule {
rules = append(rules, rule)
}
+ sort.Slice(rules, func(i, j int) bool {
+ return rules[i].ID.Timestamp().Before(rules[j].ID.Timestamp())
+ })
+
return rules
}
@@ -305,10 +333,10 @@ func (rm *rulesManagerImpl) validateAndAddRuleLocal(rule *Rule) error {
duplicatePatterns[regex] = true
}
- startId := len(rm.patterns)
+ startID := len(rm.patterns)
for id, pattern := range newPatterns {
rm.patterns = append(rm.patterns, pattern)
- rm.patternsIds[pattern.String()] = uint(startId + id)
+ rm.patternsIds[pattern.String()] = uint(startID + id)
}
rm.rules[rule.ID] = *rule
@@ -323,11 +351,13 @@ func (rm *rulesManagerImpl) generateDatabase(version RowID) error {
return err
}
- rm.databaseUpdated <- RulesDatabase{
- database: database,
- databaseSize: len(rm.patterns),
- version: version,
- }
+ go func() {
+ rm.databaseUpdated <- RulesDatabase{
+ database: database,
+ databaseSize: len(rm.patterns),
+ version: version,
+ }
+ }()
return nil
}
diff --git a/rules_manager_test.go b/rules_manager_test.go
index a2ec501..215e601 100644
--- a/rules_manager_test.go
+++ b/rules_manager_test.go
@@ -1,3 +1,20 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
package main
import (
@@ -14,7 +31,8 @@ func TestAddAndGetAllRules(t *testing.T) {
rulesManager, err := LoadRulesManager(wrapper.Storage, "FLAG{test}")
require.NoError(t, err)
impl := rulesManager.(*rulesManagerImpl)
- checkVersion(t, rulesManager, impl.rulesByName["flag"].ID)
+ checkVersion(t, rulesManager, impl.rulesByName["flag_out"].ID)
+ checkVersion(t, rulesManager, impl.rulesByName["flag_in"].ID)
emptyRule := Rule{Name: "empty", Color: "#fff", Enabled: true}
emptyID, err := rulesManager.AddRule(wrapper.Context, emptyRule)
assert.NoError(t, err)
@@ -103,8 +121,8 @@ func TestAddAndGetAllRules(t *testing.T) {
assert.Equal(t, expected, impl.rulesByName[expected.Name])
}
- assert.Len(t, impl.rules, 5)
- assert.Len(t, impl.rulesByName, 5)
+ assert.Len(t, impl.rules, 6)
+ assert.Len(t, impl.rulesByName, 6)
assert.Len(t, impl.patterns, 5)
assert.Len(t, impl.patternsIds, 5)
@@ -118,8 +136,9 @@ func TestAddAndGetAllRules(t *testing.T) {
checkRule(rule2, []int{2, 3})
checkRule(rule3, []int{3, 4})
- assert.Len(t, rulesManager.GetRules(), 5)
- assert.ElementsMatch(t, []Rule{impl.rulesByName["flag"], emptyRule, rule1, rule2, rule3}, rulesManager.GetRules())
+ assert.Len(t, rulesManager.GetRules(), 6)
+ assert.ElementsMatch(t, []Rule{impl.rulesByName["flag_out"], impl.rulesByName["flag_in"], emptyRule,
+ rule1, rule2, rule3}, rulesManager.GetRules())
wrapper.Destroy(t)
}
@@ -193,7 +212,8 @@ func TestFillWithMatchedRules(t *testing.T) {
rulesManager, err := LoadRulesManager(wrapper.Storage, "FLAG{test}")
require.NoError(t, err)
impl := rulesManager.(*rulesManagerImpl)
- checkVersion(t, rulesManager, impl.rulesByName["flag"].ID)
+ checkVersion(t, rulesManager, impl.rulesByName["flag_out"].ID)
+ checkVersion(t, rulesManager, impl.rulesByName["flag_in"].ID)
emptyRule, err := rulesManager.AddRule(wrapper.Context, Rule{Name: "empty", Color: "#fff"})
require.NoError(t, err)
diff --git a/scripts/generate_ping.py b/scripts/generate_ping.py
index 73454c1..0c06a9e 100755
--- a/scripts/generate_ping.py
+++ b/scripts/generate_ping.py
@@ -28,7 +28,7 @@ if __name__ == "__main__":
port = 9999
n = 10000
-
+
if sys.argv[1] == "server":
# docker run -it --rm -p 9999:9999 -v "$PWD":/ping -w /ping python:3 python generate_ping.py server
with socketserver.TCPServer(("0.0.0.0", port), PongHandler) as server:
diff --git a/search_controller.go b/search_controller.go
new file mode 100644
index 0000000..5ed762a
--- /dev/null
+++ b/search_controller.go
@@ -0,0 +1,211 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package main
+
+import (
+ "context"
+ log "github.com/sirupsen/logrus"
+ "strings"
+ "sync"
+ "time"
+)
+
+const (
+ secondsToNano = 1000 * 1000 * 1000
+ maxSearchTimeout = 10 * secondsToNano
+ maxRecentSearches = 200
+)
+
+type PerformedSearch struct {
+ ID RowID `bson:"_id" json:"id"`
+ SearchOptions SearchOptions `bson:"search_options" json:"search_options"`
+ AffectedConnections []RowID `bson:"affected_connections" json:"-"`
+ AffectedConnectionsCount int `bson:"affected_connections_count" json:"affected_connections_count"`
+ StartedAt time.Time `bson:"started_at" json:"started_at"`
+ FinishedAt time.Time `bson:"finished_at" json:"finished_at"`
+ UpdatedAt time.Time `bson:"updated_at" json:"updated_at"`
+ Timeout time.Duration `bson:"timeout" json:"timeout"`
+}
+
+type SearchOptions struct {
+ TextSearch TextSearch `bson:"text_search" json:"text_search"`
+ RegexSearch RegexSearch `bson:"regex_search" json:"regex_search"`
+ Timeout time.Duration `bson:"timeout" json:"timeout" binding:"max=60"`
+}
+
+type TextSearch struct {
+ Terms []string `bson:"terms" json:"terms" binding:"isdefault|min=1,dive,min=3"`
+ ExcludedTerms []string `bson:"excluded_terms" json:"excluded_terms" binding:"isdefault|min=1,dive,min=3"`
+ ExactPhrase string `bson:"exact_phrase" json:"exact_phrase" binding:"isdefault|min=3"`
+ CaseSensitive bool `bson:"case_sensitive" json:"case_sensitive"`
+}
+
+type RegexSearch struct {
+ Pattern string `bson:"pattern" json:"pattern" binding:"isdefault|min=3"`
+ NotPattern string `bson:"not_pattern" json:"not_pattern" binding:"isdefault|min=3"`
+ CaseInsensitive bool `bson:"case_insensitive" json:"case_insensitive"`
+ MultiLine bool `bson:"multi_line" json:"multi_line"`
+ IgnoreWhitespaces bool `bson:"ignore_whitespaces" json:"ignore_whitespaces"`
+ DotCharacter bool `bson:"dot_character" json:"dot_character"`
+}
+
+type SearchController struct {
+ storage Storage
+ performedSearches []PerformedSearch
+ mutex sync.Mutex
+}
+
+func NewSearchController(storage Storage) *SearchController {
+ var searches []PerformedSearch
+ if err := storage.Find(Searches).Limit(maxRecentSearches).All(&searches); err != nil {
+ log.WithError(err).Panic("failed to retrieve performed searches")
+ }
+
+ if searches == nil {
+ searches = []PerformedSearch{}
+ }
+
+ return &SearchController{
+ storage: storage,
+ performedSearches: searches,
+ }
+}
+
+func (sc *SearchController) GetPerformedSearches() []PerformedSearch {
+ sc.mutex.Lock()
+ defer sc.mutex.Unlock()
+
+ return sc.performedSearches
+}
+
+func (sc *SearchController) GetPerformedSearch(id RowID) PerformedSearch {
+ sc.mutex.Lock()
+ defer sc.mutex.Unlock()
+
+ var performedSearch PerformedSearch
+ for _, search := range sc.performedSearches {
+ if search.ID == id {
+ performedSearch = search
+ }
+ }
+
+ return performedSearch
+}
+
+func (sc *SearchController) PerformSearch(c context.Context, options SearchOptions) PerformedSearch {
+ findQuery := sc.storage.Find(ConnectionStreams).Projection(OrderedDocument{{"connection_id", 1}}).Context(c)
+ timeout := options.Timeout * secondsToNano
+ if timeout <= 0 || timeout > maxSearchTimeout {
+ timeout = maxSearchTimeout
+ }
+ findQuery = findQuery.MaxTime(timeout)
+
+ if !options.TextSearch.isZero() {
+ var text string
+ if options.TextSearch.ExactPhrase != "" {
+ text = "\"" + options.TextSearch.ExactPhrase + "\""
+ } else {
+ text = strings.Join(options.TextSearch.Terms, " ")
+ if options.TextSearch.ExcludedTerms != nil {
+ text += " -" + strings.Join(options.TextSearch.ExcludedTerms, " -")
+ }
+ }
+
+ findQuery = findQuery.Filter(OrderedDocument{{"$text", UnorderedDocument{
+ "$search": text,
+ "$language": "none",
+ "$caseSensitive": options.TextSearch.CaseSensitive,
+ "$diacriticSensitive": false,
+ }}})
+ } else {
+ var regexOptions string
+ if options.RegexSearch.CaseInsensitive {
+ regexOptions += "i"
+ }
+ if options.RegexSearch.MultiLine {
+ regexOptions += "m"
+ }
+ if options.RegexSearch.IgnoreWhitespaces {
+ regexOptions += "x"
+ }
+ if options.RegexSearch.DotCharacter {
+ regexOptions += "s"
+ }
+
+ var regex UnorderedDocument
+ if options.RegexSearch.Pattern != "" {
+ regex = UnorderedDocument{"$regex": options.RegexSearch.Pattern, "$options": regexOptions}
+ } else {
+ regex = UnorderedDocument{"$not":
+ UnorderedDocument{"$regex": options.RegexSearch.NotPattern, "$options": regexOptions}}
+ }
+
+ findQuery = findQuery.Filter(OrderedDocument{{"payload_string", regex}})
+ }
+
+ var connections []ConnectionStream
+ startedAt := time.Now()
+ if err := findQuery.All(&connections); err != nil {
+ log.WithError(err).Error("oh no")
+ }
+ affectedConnections := uniqueConnectionIds(connections)
+
+ finishedAt := time.Now()
+ performedSearch := PerformedSearch{
+ ID: NewRowID(),
+ SearchOptions: options,
+ AffectedConnections: affectedConnections,
+ AffectedConnectionsCount: len(affectedConnections),
+ StartedAt: startedAt,
+ FinishedAt: finishedAt,
+ UpdatedAt: finishedAt,
+ Timeout: options.Timeout,
+ }
+ if _, err := sc.storage.Insert(Searches).Context(c).One(performedSearch); err != nil {
+ log.WithError(err).Panic("failed to insert a new performed search")
+ }
+
+ sc.mutex.Lock()
+ sc.performedSearches = append([]PerformedSearch{performedSearch}, sc.performedSearches...)
+ if len(sc.performedSearches) > maxRecentSearches {
+ sc.performedSearches = sc.performedSearches[:200]
+ }
+ sc.mutex.Unlock()
+
+ return performedSearch
+}
+
+func (sc TextSearch) isZero() bool {
+ return sc.Terms == nil && sc.ExcludedTerms == nil && sc.ExactPhrase == ""
+}
+
+func (sc RegexSearch) isZero() bool {
+ return RegexSearch{} == sc
+}
+
+func uniqueConnectionIds(connections []ConnectionStream) []RowID {
+ keys := make(map[RowID]bool)
+ var out []RowID
+ for _, entry := range connections {
+ if _, value := keys[entry.ConnectionID]; !value {
+ keys[entry.ConnectionID] = true
+ out = append(out, entry.ConnectionID)
+ }
+ }
+ return out
+}
diff --git a/services_controller.go b/services_controller.go
index 9907b5e..e5fa200 100644
--- a/services_controller.go
+++ b/services_controller.go
@@ -1,3 +1,20 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
package main
import (
diff --git a/statistics_controller.go b/statistics_controller.go
new file mode 100644
index 0000000..29f3fec
--- /dev/null
+++ b/statistics_controller.go
@@ -0,0 +1,103 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package main
+
+import (
+ "context"
+ "fmt"
+ log "github.com/sirupsen/logrus"
+ "time"
+)
+
+type StatisticRecord struct {
+ RangeStart time.Time `json:"range_start" bson:"_id"`
+ ConnectionsPerService map[uint16]int `json:"connections_per_service" bson:"connections_per_service"`
+ ClientBytesPerService map[uint16]int `json:"client_bytes_per_service" bson:"client_bytes_per_service"`
+ ServerBytesPerService map[uint16]int `json:"server_bytes_per_service" bson:"server_bytes_per_service"`
+ TotalBytesPerService map[uint16]int `json:"total_bytes_per_service" bson:"total_bytes_per_service"`
+ DurationPerService map[uint16]int64 `json:"duration_per_service" bson:"duration_per_service"`
+ MatchedRules map[string]int64 `json:"matched_rules" bson:"matched_rules"`
+}
+
+type StatisticsFilter struct {
+ RangeFrom time.Time `form:"range_from"`
+ RangeTo time.Time `form:"range_to"`
+ Ports []uint16 `form:"ports"`
+ RulesIDs []string `form:"rules_ids"`
+ Metric string `form:"metric"`
+}
+
+type StatisticsController struct {
+ storage Storage
+ servicesMetrics []string
+}
+
+func NewStatisticsController(storage Storage) StatisticsController {
+ return StatisticsController{
+ storage: storage,
+ servicesMetrics: []string{"connections_per_service", "client_bytes_per_service",
+ "server_bytes_per_service", "total_bytes_per_service", "duration_per_service"},
+ }
+}
+
+func (sc *StatisticsController) GetStatistics(context context.Context, filter StatisticsFilter) []StatisticRecord {
+ var statisticRecords []StatisticRecord
+ query := sc.storage.Find(Statistics).Context(context).Sort("_id", true)
+ if !filter.RangeFrom.IsZero() {
+ query = query.Filter(OrderedDocument{{"_id", UnorderedDocument{"$lt": filter.RangeFrom}}})
+ }
+ if !filter.RangeTo.IsZero() {
+ query = query.Filter(OrderedDocument{{"_id", UnorderedDocument{"$gt": filter.RangeTo}}})
+ }
+ for _, port := range filter.Ports {
+ for _, metric := range sc.servicesMetrics {
+ if filter.Metric == "" || filter.Metric == metric {
+ query = query.Projection(OrderedDocument{{fmt.Sprintf("%s.%d", metric, port), 1}})
+ }
+ }
+
+ }
+ if filter.Metric != "" && len(filter.Ports) == 0 {
+ for _, metric := range sc.servicesMetrics {
+ if filter.Metric == metric {
+ query = query.Projection(OrderedDocument{{metric, 1}})
+ }
+ }
+ }
+ for _, ruleID := range filter.RulesIDs {
+ if filter.Metric == "" || filter.Metric == "matched_rules" {
+ query = query.Projection(OrderedDocument{{fmt.Sprintf("matched_rules.%s", ruleID), 1}})
+ }
+
+ }
+ if filter.Metric != "" && len(filter.RulesIDs) == 0 {
+ if filter.Metric == "matched_rules" {
+ query = query.Projection(OrderedDocument{{"matched_rules", 1}})
+ }
+ }
+
+ if err := query.All(&statisticRecords); err != nil {
+ log.WithError(err).WithField("filter", filter).Error("failed to retrieve statistics")
+ return []StatisticRecord{}
+ }
+ if statisticRecords == nil {
+ return []StatisticRecord{}
+ }
+
+ return statisticRecords
+}
diff --git a/storage.go b/storage.go
index aced06b..8505bfe 100644
--- a/storage.go
+++ b/storage.go
@@ -1,3 +1,20 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
package main
import (
@@ -8,15 +25,20 @@ import (
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
+ "time"
)
// Collections names
-const Connections = "connections"
-const ConnectionStreams = "connection_streams"
-const ImportingSessions = "importing_sessions"
-const Rules = "rules"
-const Settings = "settings"
-const Services = "services"
+const (
+ Connections = "connections"
+ ConnectionStreams = "connection_streams"
+ ImportingSessions = "importing_sessions"
+ Rules = "rules"
+ Searches = "searches"
+ Settings = "settings"
+ Services = "services"
+ Statistics = "statistics"
+)
var ZeroRowID [12]byte
@@ -55,8 +77,10 @@ func NewMongoStorage(uri string, port int, database string) (*MongoStorage, erro
ConnectionStreams: db.Collection(ConnectionStreams),
ImportingSessions: db.Collection(ImportingSessions),
Rules: db.Collection(Rules),
+ Searches: db.Collection(Searches),
Settings: db.Collection(Settings),
Services: db.Collection(Services),
+ Statistics: db.Collection(Statistics),
}
if _, err := collections[Services].Indexes().CreateOne(ctx, mongo.IndexModel{
@@ -66,9 +90,13 @@ func NewMongoStorage(uri string, port int, database string) (*MongoStorage, erro
return nil, err
}
- if _, err := collections[ConnectionStreams].Indexes().CreateOne(ctx, mongo.IndexModel{
- Keys: bson.D{{"connection_id", -1}}, // descending
- Options: options.Index(),
+ if _, err := collections[ConnectionStreams].Indexes().CreateMany(ctx, []mongo.IndexModel{
+ {
+ Keys: bson.D{{"connection_id", -1}}, // descending
+ },
+ {
+ Keys: bson.D{{"payload_string", "text"}},
+ },
}); err != nil {
return nil, err
}
@@ -150,6 +178,7 @@ type UpdateOperation interface {
Filter(filter OrderedDocument) UpdateOperation
Upsert(upsertResults *interface{}) UpdateOperation
One(update interface{}) (bool, error)
+ OneComplex(update interface{}) (bool, error)
Many(update interface{}) (int64, error)
}
@@ -200,6 +229,22 @@ func (fo MongoUpdateOperation) One(update interface{}) (bool, error) {
return result.ModifiedCount == 1, nil
}
+func (fo MongoUpdateOperation) OneComplex(update interface{}) (bool, error) {
+ if fo.err != nil {
+ return false, fo.err
+ }
+
+ result, err := fo.collection.UpdateOne(fo.ctx, fo.filter, update, fo.opt)
+ if err != nil {
+ return false, err
+ }
+
+ if fo.upsertResult != nil {
+ *(fo.upsertResult) = result.UpsertedID
+ }
+ return result.ModifiedCount == 1, nil
+}
+
func (fo MongoUpdateOperation) Many(update interface{}) (int64, error) {
if fo.err != nil {
return 0, fo.err
@@ -238,8 +283,11 @@ func (storage *MongoStorage) Update(collectionName string) UpdateOperation {
type FindOperation interface {
Context(ctx context.Context) FindOperation
Filter(filter OrderedDocument) FindOperation
+ Projection(filter OrderedDocument) FindOperation
Sort(field string, ascending bool) FindOperation
Limit(n int64) FindOperation
+ Skip(n int64) FindOperation
+ MaxTime(duration time.Duration) FindOperation
First(result interface{}) error
All(results interface{}) error
}
@@ -247,6 +295,7 @@ type FindOperation interface {
type MongoFindOperation struct {
collection *mongo.Collection
filter OrderedDocument
+ projection OrderedDocument
ctx context.Context
optFind *options.FindOptions
optFindOne *options.FindOneOptions
@@ -266,11 +315,30 @@ func (fo MongoFindOperation) Filter(filter OrderedDocument) FindOperation {
return fo
}
+func (fo MongoFindOperation) Projection(projection OrderedDocument) FindOperation {
+ for _, elem := range projection {
+ fo.projection = append(fo.projection, primitive.E{Key: elem.Key, Value: elem.Value})
+ }
+ fo.optFindOne.SetProjection(fo.projection)
+ fo.optFind.SetProjection(fo.projection)
+ return fo
+}
+
func (fo MongoFindOperation) Limit(n int64) FindOperation {
fo.optFind.SetLimit(n)
return fo
}
+func (fo MongoFindOperation) Skip(n int64) FindOperation {
+ fo.optFind.SetSkip(n)
+ return fo
+}
+
+func (fo MongoFindOperation) MaxTime(duration time.Duration) FindOperation {
+ fo.optFind.SetMaxTime(duration)
+ return fo
+}
+
func (fo MongoFindOperation) Sort(field string, ascending bool) FindOperation {
var sort int
if ascending {
@@ -321,6 +389,7 @@ func (storage *MongoStorage) Find(collectionName string) FindOperation {
op := MongoFindOperation{
collection: collection,
filter: OrderedDocument{},
+ projection: OrderedDocument{},
optFind: options.Find(),
optFindOne: options.FindOne(),
sorts: OrderedDocument{},
diff --git a/storage_test.go b/storage_test.go
index 4caa30d..dd91e97 100644
--- a/storage_test.go
+++ b/storage_test.go
@@ -1,3 +1,20 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
package main
import (
diff --git a/stream_handler.go b/stream_handler.go
index bccdeee..f08bd70 100644
--- a/stream_handler.go
+++ b/stream_handler.go
@@ -1,3 +1,20 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
package main
import (
@@ -5,6 +22,7 @@ import (
"github.com/flier/gohs/hyperscan"
"github.com/google/gopacket/tcpassembly"
log "github.com/sirupsen/logrus"
+ "strings"
"time"
)
@@ -159,6 +177,7 @@ func (sh *StreamHandler) storageCurrentDocument() {
ConnectionID: ZeroRowID,
DocumentIndex: len(sh.documentsIDs),
Payload: sh.buffer.Bytes(),
+ PayloadString: strings.ToValidUTF8(string(sh.buffer.Bytes()), ""),
BlocksIndexes: sh.indexes,
BlocksTimestamps: sh.timestamps,
BlocksLoss: sh.lossBlocks,
diff --git a/stream_handler_test.go b/stream_handler_test.go
index 199ae5b..127aa82 100644
--- a/stream_handler_test.go
+++ b/stream_handler_test.go
@@ -1,3 +1,20 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
package main
import (
diff --git a/utils.go b/utils.go
index a14fdca..e721d78 100644
--- a/utils.go
+++ b/utils.go
@@ -1,3 +1,20 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
package main
import (
@@ -40,12 +57,12 @@ func CustomRowID(payload uint64, timestamp time.Time) RowID {
binary.BigEndian.PutUint32(key[0:4], uint32(timestamp.Unix()))
binary.BigEndian.PutUint64(key[4:12], payload)
- if oid, err := primitive.ObjectIDFromHex(hex.EncodeToString(key[:])); err == nil {
+ oid, err := primitive.ObjectIDFromHex(hex.EncodeToString(key[:]))
+ if err == nil {
return oid
- } else {
- log.WithError(err).Warn("failed to create object id")
- return primitive.NewObjectID()
}
+ log.WithError(err).Warn("failed to create object id")
+ return primitive.NewObjectID()
}
func NewRowID() RowID {
@@ -151,3 +168,11 @@ func ParseIPNet(address string) *net.IPNet {
return network
}
+
+func Average(array []float64) float64 {
+ var sum float64
+ for _, f := range array {
+ sum += f
+ }
+ return sum / float64(len(array))
+}