From 584e25f7940954a51e260a21ca5a819ff4b834d3 Mon Sep 17 00:00:00 2001 From: Emiliano Ciavatta Date: Thu, 8 Oct 2020 14:59:50 +0200 Subject: Implement download_as and export as pwntools --- connection_streams_controller.go | 178 +++++++++++++++++++++++++++++++++++---- 1 file changed, 162 insertions(+), 16 deletions(-) (limited to 'connection_streams_controller.go') diff --git a/connection_streams_controller.go b/connection_streams_controller.go index 9d73b0e..98f2aca 100644 --- a/connection_streams_controller.go +++ b/connection_streams_controller.go @@ -3,14 +3,19 @@ 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 ( + initialPayloadsSize = 1024 + defaultQueryFormatLimit = 8024 + initialRegexSlicesCount = 8 + pwntoolsMaxServerBytes = 20 +) type ConnectionStream struct { ID RowID `bson:"_id"` @@ -26,7 +31,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,12 +47,17 @@ type RegexSlice struct { To uint64 `json:"to"` } -type QueryFormat struct { +type GetMessageFormat struct { Format string `form:"format"` Skip uint64 `form:"skip"` Limit uint64 `form:"limit"` } +type DownloadMessageFormat struct { + Format string `form:"format"` + Type string `form:"type"` +} + type ConnectionStreamsController struct { storage Storage } @@ -58,13 +68,18 @@ func NewConnectionStreamsController(storage Storage) ConnectionStreamsController } } -func (csc ConnectionStreamsController) GetConnectionPayload(c context.Context, connectionID RowID, - format QueryFormat) []*Payload { - payloads := make([]*Payload, 0, InitialPayloadsSize) +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 + } + + payloads := make([]*Message, 0, initialPayloadsSize) var clientIndex, serverIndex, globalIndex uint64 if format.Limit <= 0 { - format.Limit = DefaultQueryFormatLimit + format.Limit = defaultQueryFormatLimit } var clientBlocksIndex, serverBlocksIndex int @@ -79,8 +94,8 @@ func (csc ConnectionStreamsController) GetConnectionPayload(c context.Context, c return serverBlocksIndex < len(serverStream.BlocksIndexes) } - var payload *Payload - payloadsBuffer := make([]*Payload, 0, 16) + var payload *Message + payloadsBuffer := make([]*Message, 0, 16) contentChunkBuffer := new(bytes.Buffer) var lastContentSlice []byte var sideChanged, lastClient, lastServer bool @@ -97,7 +112,7 @@ func (csc ConnectionStreamsController) GetConnectionPayload(c context.Context, c } size := uint64(end - start) - payload = &Payload{ + payload = &Message{ FromClient: true, Content: DecodeBytes(clientStream.Payload[start:end], format.Format), Index: start, @@ -121,7 +136,7 @@ func (csc ConnectionStreamsController) GetConnectionPayload(c context.Context, c } size := uint64(end - start) - payload = &Payload{ + payload = &Message{ FromClient: false, Content: DecodeBytes(serverStream.Payload[start:end], format.Format), Index: start, @@ -178,11 +193,118 @@ func (csc ConnectionStreamsController) GetConnectionPayload(c context.Context, c 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 + return payloads, true } } - return payloads + return payloads, 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") + } + 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 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,7 +321,7 @@ 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] { @@ -225,3 +347,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) + } else { + return fmt.Sprintf("p.recvuntil(%s)\n", content) + } +} -- cgit v1.2.3-70-g09d2 From a30815021e61023f996b1450ddcd9164a6e18bef Mon Sep 17 00:00:00 2001 From: Emiliano Ciavatta Date: Thu, 8 Oct 2020 17:07:07 +0200 Subject: Add header license to all files --- application_context.go | 17 +++++++++++++++++ application_context_test.go | 17 +++++++++++++++++ application_router.go | 17 +++++++++++++++++ application_router_test.go | 17 +++++++++++++++++ caronte.go | 17 +++++++++++++++++ caronte_test.go | 17 +++++++++++++++++ connection_handler.go | 17 +++++++++++++++++ connection_handler_test.go | 17 +++++++++++++++++ connection_streams_controller.go | 17 +++++++++++++++++ connections_controller.go | 17 +++++++++++++++++ frontend/src/backend.js | 17 +++++++++++++++++ frontend/src/components/Connection.js | 17 +++++++++++++++++ frontend/src/components/ConnectionContent.js | 17 +++++++++++++++++ frontend/src/components/ConnectionMatchedRules.js | 17 +++++++++++++++++ frontend/src/components/MessageAction.js | 17 +++++++++++++++++ frontend/src/components/Notifications.js | 17 +++++++++++++++++ frontend/src/components/fields/ButtonField.js | 17 +++++++++++++++++ frontend/src/components/fields/CheckField.js | 17 +++++++++++++++++ frontend/src/components/fields/ChoiceField.js | 17 +++++++++++++++++ frontend/src/components/fields/InputField.js | 17 +++++++++++++++++ frontend/src/components/fields/TextField.js | 17 +++++++++++++++++ .../src/components/fields/extensions/ColorField.js | 17 +++++++++++++++++ .../components/fields/extensions/NumericField.js | 17 +++++++++++++++++ .../components/filters/BooleanConnectionsFilter.js | 17 +++++++++++++++++ .../src/components/filters/FiltersDefinitions.js | 17 +++++++++++++++++ .../components/filters/RulesConnectionsFilter.js | 17 +++++++++++++++++ .../components/filters/StringConnectionsFilter.js | 17 +++++++++++++++++ frontend/src/components/objects/LinkPopover.js | 17 +++++++++++++++++ frontend/src/components/panels/ConfigurationPane.js | 17 +++++++++++++++++ frontend/src/components/panels/MainPane.js | 17 +++++++++++++++++ frontend/src/components/panels/PcapPane.js | 17 +++++++++++++++++ frontend/src/components/panels/RulePane.js | 17 +++++++++++++++++ frontend/src/components/panels/ServicePane.js | 17 +++++++++++++++++ frontend/src/dispatcher.js | 16 ++++++++++++++++ frontend/src/index.js | 17 +++++++++++++++++ frontend/src/log.js | 17 +++++++++++++++++ frontend/src/notifications.js | 17 +++++++++++++++++ frontend/src/setupProxy.js | 17 +++++++++++++++++ frontend/src/utils.js | 17 +++++++++++++++++ frontend/src/validation.js | 16 ++++++++++++++++ frontend/src/views/App.js | 17 +++++++++++++++++ frontend/src/views/Connections.js | 17 +++++++++++++++++ frontend/src/views/Filters.js | 21 +++++++++++++++++++-- frontend/src/views/Header.js | 17 +++++++++++++++++ frontend/src/views/Timeline.js | 17 +++++++++++++++++ notification_controller.go | 17 +++++++++++++++++ parsers/http_request_parser.go | 17 +++++++++++++++++ parsers/http_response_parser.go | 17 +++++++++++++++++ parsers/parser.go | 17 +++++++++++++++++ parsers/parser_utils.go | 17 +++++++++++++++++ pcap_importer.go | 17 +++++++++++++++++ pcap_importer_test.go | 17 +++++++++++++++++ resources_controller.go | 17 +++++++++++++++++ rules_manager.go | 17 +++++++++++++++++ rules_manager_test.go | 17 +++++++++++++++++ services_controller.go | 17 +++++++++++++++++ statistics_controller.go | 17 +++++++++++++++++ storage.go | 17 +++++++++++++++++ storage_test.go | 17 +++++++++++++++++ stream_handler.go | 17 +++++++++++++++++ stream_handler_test.go | 17 +++++++++++++++++ utils.go | 17 +++++++++++++++++ 62 files changed, 1054 insertions(+), 2 deletions(-) (limited to 'connection_streams_controller.go') diff --git a/application_context.go b/application_context.go index 9a9c97a..9897bb6 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 . + */ + package main import ( diff --git a/application_context_test.go b/application_context_test.go index 28c81a5..a7f1a49 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 . + */ + package main import ( diff --git a/application_router.go b/application_router.go index da71538..9fd7e3d 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 . + */ + package main import ( diff --git a/application_router_test.go b/application_router_test.go index f4804e3..9741eed 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 . + */ + package main import ( diff --git a/caronte.go b/caronte.go index d999724..d4265bc 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 . + */ + package main import ( 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 . + */ + package main import ( diff --git a/connection_handler.go b/connection_handler.go index 3d38531..6b2b411 100644 --- a/connection_handler.go +++ b/connection_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 . + */ + package main import ( 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 . + */ + package main import ( diff --git a/connection_streams_controller.go b/connection_streams_controller.go index 98f2aca..9251a3a 100644 --- a/connection_streams_controller.go +++ b/connection_streams_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 . + */ + package main import ( diff --git a/connections_controller.go b/connections_controller.go index e872c9f..30a5ee5 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 . + */ + package main import ( diff --git a/frontend/src/backend.js b/frontend/src/backend.js index 1b2d8d2..cc8604a 100644 --- a/frontend/src/backend.js +++ b/frontend/src/backend.js @@ -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 . + */ + async function json(method, url, data, json, headers) { const options = { method: method, diff --git a/frontend/src/components/Connection.js b/frontend/src/components/Connection.js index b7e2531..c7b0010 100644 --- a/frontend/src/components/Connection.js +++ b/frontend/src/components/Connection.js @@ -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 . + */ + import React, {Component} from 'react'; import './Connection.scss'; import {Form, OverlayTrigger, Popover} from "react-bootstrap"; diff --git a/frontend/src/components/ConnectionContent.js b/frontend/src/components/ConnectionContent.js index b09dcf3..b468277 100644 --- a/frontend/src/components/ConnectionContent.js +++ b/frontend/src/components/ConnectionContent.js @@ -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 . + */ + import React, {Component} from 'react'; import './ConnectionContent.scss'; import {Row} from 'react-bootstrap'; diff --git a/frontend/src/components/ConnectionMatchedRules.js b/frontend/src/components/ConnectionMatchedRules.js index 21f2a92..35643c5 100644 --- a/frontend/src/components/ConnectionMatchedRules.js +++ b/frontend/src/components/ConnectionMatchedRules.js @@ -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 . + */ + import React, {Component} from 'react'; import './ConnectionMatchedRules.scss'; import ButtonField from "./fields/ButtonField"; diff --git a/frontend/src/components/MessageAction.js b/frontend/src/components/MessageAction.js index 8f4b031..b94cbb9 100644 --- a/frontend/src/components/MessageAction.js +++ b/frontend/src/components/MessageAction.js @@ -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 . + */ + import React, {Component} from 'react'; import './MessageAction.scss'; import {Modal} from "react-bootstrap"; diff --git a/frontend/src/components/Notifications.js b/frontend/src/components/Notifications.js index 9ce2b58..1017a42 100644 --- a/frontend/src/components/Notifications.js +++ b/frontend/src/components/Notifications.js @@ -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 . + */ + import React, {Component} from 'react'; import './Notifications.scss'; import dispatcher from "../dispatcher"; diff --git a/frontend/src/components/fields/ButtonField.js b/frontend/src/components/fields/ButtonField.js index cc32b0f..ffcceae 100644 --- a/frontend/src/components/fields/ButtonField.js +++ b/frontend/src/components/fields/ButtonField.js @@ -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 . + */ + import React, {Component} from 'react'; import './ButtonField.scss'; import './common.scss'; diff --git a/frontend/src/components/fields/CheckField.js b/frontend/src/components/fields/CheckField.js index 33f4f83..dd44970 100644 --- a/frontend/src/components/fields/CheckField.js +++ b/frontend/src/components/fields/CheckField.js @@ -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 . + */ + import React, {Component} from 'react'; import './CheckField.scss'; import './common.scss'; diff --git a/frontend/src/components/fields/ChoiceField.js b/frontend/src/components/fields/ChoiceField.js index 73e950d..14071c3 100644 --- a/frontend/src/components/fields/ChoiceField.js +++ b/frontend/src/components/fields/ChoiceField.js @@ -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 . + */ + import React, {Component} from 'react'; import './ChoiceField.scss'; import './common.scss'; diff --git a/frontend/src/components/fields/InputField.js b/frontend/src/components/fields/InputField.js index 84c981b..80cce3b 100644 --- a/frontend/src/components/fields/InputField.js +++ b/frontend/src/components/fields/InputField.js @@ -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 . + */ + import React, {Component} from 'react'; import './InputField.scss'; import './common.scss'; diff --git a/frontend/src/components/fields/TextField.js b/frontend/src/components/fields/TextField.js index de68c21..9237c0c 100644 --- a/frontend/src/components/fields/TextField.js +++ b/frontend/src/components/fields/TextField.js @@ -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 . + */ + import React, {Component} from 'react'; import './TextField.scss'; import './common.scss'; diff --git a/frontend/src/components/fields/extensions/ColorField.js b/frontend/src/components/fields/extensions/ColorField.js index 96ebc49..f1c0caf 100644 --- a/frontend/src/components/fields/extensions/ColorField.js +++ b/frontend/src/components/fields/extensions/ColorField.js @@ -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 . + */ + import React, {Component} from 'react'; import {OverlayTrigger, Popover} from "react-bootstrap"; import './ColorField.scss'; diff --git a/frontend/src/components/fields/extensions/NumericField.js b/frontend/src/components/fields/extensions/NumericField.js index 19a9e46..d4d027d 100644 --- a/frontend/src/components/fields/extensions/NumericField.js +++ b/frontend/src/components/fields/extensions/NumericField.js @@ -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 . + */ + import React, {Component} from 'react'; import InputField from "../InputField"; diff --git a/frontend/src/components/filters/BooleanConnectionsFilter.js b/frontend/src/components/filters/BooleanConnectionsFilter.js index 4c5a78a..a9a420e 100644 --- a/frontend/src/components/filters/BooleanConnectionsFilter.js +++ b/frontend/src/components/filters/BooleanConnectionsFilter.js @@ -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 . + */ + import React, {Component} from 'react'; import {withRouter} from "react-router-dom"; import {Redirect} from "react-router"; diff --git a/frontend/src/components/filters/FiltersDefinitions.js b/frontend/src/components/filters/FiltersDefinitions.js index d4f2912..cde3cfb 100644 --- a/frontend/src/components/filters/FiltersDefinitions.js +++ b/frontend/src/components/filters/FiltersDefinitions.js @@ -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 . + */ + import {cleanNumber, validateIpAddress, validateMin, validatePort} from "../../utils"; import StringConnectionsFilter from "./StringConnectionsFilter"; import React from "react"; diff --git a/frontend/src/components/filters/RulesConnectionsFilter.js b/frontend/src/components/filters/RulesConnectionsFilter.js index 8366189..48affb0 100644 --- a/frontend/src/components/filters/RulesConnectionsFilter.js +++ b/frontend/src/components/filters/RulesConnectionsFilter.js @@ -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 . + */ + import React, {Component} from 'react'; import {withRouter} from "react-router-dom"; import {Redirect} from "react-router"; diff --git a/frontend/src/components/filters/StringConnectionsFilter.js b/frontend/src/components/filters/StringConnectionsFilter.js index f463593..a3b45dc 100644 --- a/frontend/src/components/filters/StringConnectionsFilter.js +++ b/frontend/src/components/filters/StringConnectionsFilter.js @@ -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 . + */ + import React, {Component} from 'react'; import {withRouter} from "react-router-dom"; import {Redirect} from "react-router"; diff --git a/frontend/src/components/objects/LinkPopover.js b/frontend/src/components/objects/LinkPopover.js index 8768caa..3c5bf67 100644 --- a/frontend/src/components/objects/LinkPopover.js +++ b/frontend/src/components/objects/LinkPopover.js @@ -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 . + */ + import React, {Component} from 'react'; import {randomClassName} from "../../utils"; import {OverlayTrigger, Popover} from "react-bootstrap"; diff --git a/frontend/src/components/panels/ConfigurationPane.js b/frontend/src/components/panels/ConfigurationPane.js index 10309f6..9ae2cfb 100644 --- a/frontend/src/components/panels/ConfigurationPane.js +++ b/frontend/src/components/panels/ConfigurationPane.js @@ -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 . + */ + import React, {Component} from 'react'; import './common.scss'; import './ConfigurationPane.scss'; diff --git a/frontend/src/components/panels/MainPane.js b/frontend/src/components/panels/MainPane.js index bd25e0c..d34d58a 100644 --- a/frontend/src/components/panels/MainPane.js +++ b/frontend/src/components/panels/MainPane.js @@ -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 . + */ + import React, {Component} from 'react'; import './common.scss'; import './MainPane.scss'; diff --git a/frontend/src/components/panels/PcapPane.js b/frontend/src/components/panels/PcapPane.js index 13f7cb3..d5c2225 100644 --- a/frontend/src/components/panels/PcapPane.js +++ b/frontend/src/components/panels/PcapPane.js @@ -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 . + */ + import React, {Component} from 'react'; import './PcapPane.scss'; import './common.scss'; diff --git a/frontend/src/components/panels/RulePane.js b/frontend/src/components/panels/RulePane.js index 76f3ac0..9913962 100644 --- a/frontend/src/components/panels/RulePane.js +++ b/frontend/src/components/panels/RulePane.js @@ -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 . + */ + import React, {Component} from 'react'; import './common.scss'; import './RulePane.scss'; diff --git a/frontend/src/components/panels/ServicePane.js b/frontend/src/components/panels/ServicePane.js index 22c6655..fc7004b 100644 --- a/frontend/src/components/panels/ServicePane.js +++ b/frontend/src/components/panels/ServicePane.js @@ -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 . + */ + import React, {Component} from 'react'; import './common.scss'; import './ServicePane.scss'; diff --git a/frontend/src/dispatcher.js b/frontend/src/dispatcher.js index 4b8b5a4..943f7ec 100644 --- a/frontend/src/dispatcher.js +++ b/frontend/src/dispatcher.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 . + */ class Dispatcher { diff --git a/frontend/src/index.js b/frontend/src/index.js index beb52ae..e3e48de 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -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 . + */ + import React from 'react'; import ReactDOM from 'react-dom'; import 'bootstrap/dist/css/bootstrap.css'; diff --git a/frontend/src/log.js b/frontend/src/log.js index 0883962..424e1b4 100644 --- a/frontend/src/log.js +++ b/frontend/src/log.js @@ -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 . + */ + const log = { debug: (...obj) => console.info(...obj), info: (...obj) => console.info(...obj), diff --git a/frontend/src/notifications.js b/frontend/src/notifications.js index 2a77ffb..f04036d 100644 --- a/frontend/src/notifications.js +++ b/frontend/src/notifications.js @@ -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 . + */ + import log from "./log"; import dispatcher from "./dispatcher"; diff --git a/frontend/src/setupProxy.js b/frontend/src/setupProxy.js index 6f082c8..f2e1c39 100644 --- a/frontend/src/setupProxy.js +++ b/frontend/src/setupProxy.js @@ -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 . + */ + const { createProxyMiddleware } = require('http-proxy-middleware'); module.exports = function(app) { diff --git a/frontend/src/utils.js b/frontend/src/utils.js index aacc625..f333f0d 100644 --- a/frontend/src/utils.js +++ b/frontend/src/utils.js @@ -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 . + */ + 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) { 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 . + */ 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 index 4bb9f57..8105117 100644 --- a/frontend/src/views/App.js +++ b/frontend/src/views/App.js @@ -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 . + */ + import React, {Component} from 'react'; import './App.scss'; import Header from "./Header"; diff --git a/frontend/src/views/Connections.js b/frontend/src/views/Connections.js index e835dcb..b2edd3f 100644 --- a/frontend/src/views/Connections.js +++ b/frontend/src/views/Connections.js @@ -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 . + */ + import React, {Component} from 'react'; import './Connections.scss'; import Connection from "../components/Connection"; diff --git a/frontend/src/views/Filters.js b/frontend/src/views/Filters.js index ba7d467..3dd8280 100644 --- a/frontend/src/views/Filters.js +++ b/frontend/src/views/Filters.js @@ -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 . + */ + import React, {Component} from 'react'; import {Col, Container, Modal, Row, Table} from "react-bootstrap"; import {filtersDefinitions, filtersNames} from "../components/filters/FiltersDefinitions"; @@ -31,7 +48,7 @@ class Filters extends Component { this.checkboxChangesHandler(name, event)} /> + onChange={event => this.checkboxChangesHandler(name, event)}/> {filtersDefinitions[name]} ); @@ -89,7 +106,7 @@ class Filters extends Component { - + ); diff --git a/frontend/src/views/Header.js b/frontend/src/views/Header.js index f5eff17..2cfe9fb 100644 --- a/frontend/src/views/Header.js +++ b/frontend/src/views/Header.js @@ -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 . + */ + import React, {Component} from 'react'; import Typed from 'typed.js'; import './Header.scss'; diff --git a/frontend/src/views/Timeline.js b/frontend/src/views/Timeline.js index 3adbf88..ebe3eb9 100644 --- a/frontend/src/views/Timeline.js +++ b/frontend/src/views/Timeline.js @@ -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 . + */ + import React, {Component} from 'react'; import './Timeline.scss'; import { diff --git a/notification_controller.go b/notification_controller.go index 88c9e8c..3fa3c5b 100644 --- a/notification_controller.go +++ b/notification_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 . + */ + package main import ( diff --git a/parsers/http_request_parser.go b/parsers/http_request_parser.go index e2224b8..bc98f8f 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 . + */ + package parsers import ( diff --git a/parsers/http_response_parser.go b/parsers/http_response_parser.go index 1770116..e5ef1ac 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 . + */ + package parsers import ( diff --git a/parsers/parser.go b/parsers/parser.go index 06cc0dc..a29b1ab 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 . + */ + package parsers type Parser interface { 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 . + */ + package parsers import ( diff --git a/pcap_importer.go b/pcap_importer.go index 78a5e6c..41ed082 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 . + */ + package main import ( diff --git a/pcap_importer_test.go b/pcap_importer_test.go index be09ea9..8940060 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 . + */ + package main import ( diff --git a/resources_controller.go b/resources_controller.go index 050157a..10b31e3 100644 --- a/resources_controller.go +++ b/resources_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 . + */ + package main import ( diff --git a/rules_manager.go b/rules_manager.go index a5dc7ce..636fc74 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 . + */ + package main import ( diff --git a/rules_manager_test.go b/rules_manager_test.go index a2ec501..dded096 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 . + */ + package main import ( 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 . + */ + package main import ( diff --git a/statistics_controller.go b/statistics_controller.go index 65c7d58..006b230 100644 --- a/statistics_controller.go +++ b/statistics_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 . + */ + package main import ( diff --git a/storage.go b/storage.go index 0888ce0..f8b7f9c 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 . + */ + package main import ( 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 . + */ + package main import ( diff --git a/stream_handler.go b/stream_handler.go index bccdeee..48dba34 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 . + */ + package main import ( 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 . + */ + package main import ( diff --git a/utils.go b/utils.go index ec5a807..639fd94 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 . + */ + package main import ( -- cgit v1.2.3-70-g09d2 From d203f3c7e3bcaa20895c0f32f348cd1513ae9876 Mon Sep 17 00:00:00 2001 From: Emiliano Ciavatta Date: Thu, 8 Oct 2020 22:17:04 +0200 Subject: Frontend folder structure refactor --- connection_streams_controller.go | 44 +-- frontend/src/components/App.js | 62 +++ frontend/src/components/Connection.js | 145 ------- frontend/src/components/Connection.scss | 68 ---- frontend/src/components/ConnectionContent.js | 243 ------------ frontend/src/components/ConnectionContent.scss | 113 ------ frontend/src/components/ConnectionMatchedRules.js | 46 --- .../src/components/ConnectionMatchedRules.scss | 23 -- frontend/src/components/Header.js | 111 ++++++ frontend/src/components/Header.scss | 34 ++ frontend/src/components/MessageAction.js | 68 ---- frontend/src/components/MessageAction.scss | 8 - frontend/src/components/Timeline.js | 232 +++++++++++ frontend/src/components/Timeline.scss | 22 ++ frontend/src/components/dialogs/Filters.js | 116 ++++++ frontend/src/components/objects/Connection.js | 145 +++++++ frontend/src/components/objects/Connection.scss | 68 ++++ .../components/objects/ConnectionMatchedRules.js | 46 +++ .../components/objects/ConnectionMatchedRules.scss | 23 ++ frontend/src/components/objects/MessageAction.js | 68 ++++ frontend/src/components/objects/MessageAction.scss | 8 + frontend/src/components/pages/ConfigurationPage.js | 179 +++++++++ .../src/components/pages/ConfigurationPage.scss | 18 + frontend/src/components/pages/MainPage.js | 76 ++++ frontend/src/components/pages/MainPage.scss | 24 ++ .../src/components/pages/ServiceUnavailablePage.js | 34 ++ frontend/src/components/pages/common.scss | 16 + .../src/components/panels/ConfigurationPane.js | 179 --------- .../src/components/panels/ConfigurationPane.scss | 18 - frontend/src/components/panels/ConnectionsPane.js | 304 ++++++++++++++ .../src/components/panels/ConnectionsPane.scss | 38 ++ frontend/src/components/panels/MainPane.js | 47 +-- frontend/src/components/panels/MainPane.scss | 17 - frontend/src/components/panels/PcapPane.js | 273 ------------- frontend/src/components/panels/PcapPane.scss | 38 -- frontend/src/components/panels/PcapsPane.js | 273 +++++++++++++ frontend/src/components/panels/PcapsPane.scss | 38 ++ frontend/src/components/panels/RulePane.js | 438 --------------------- frontend/src/components/panels/RulePane.scss | 32 -- frontend/src/components/panels/RulesPane.js | 438 +++++++++++++++++++++ frontend/src/components/panels/RulesPane.scss | 32 ++ frontend/src/components/panels/ServicePane.js | 212 ---------- frontend/src/components/panels/ServicePane.scss | 22 -- frontend/src/components/panels/ServicesPane.js | 212 ++++++++++ frontend/src/components/panels/ServicesPane.scss | 22 ++ frontend/src/components/panels/StreamsPane.js | 242 ++++++++++++ frontend/src/components/panels/StreamsPane.scss | 113 ++++++ frontend/src/index.js | 2 +- frontend/src/views/App.js | 81 ---- frontend/src/views/App.scss | 16 - frontend/src/views/Connections.js | 293 -------------- frontend/src/views/Connections.scss | 38 -- frontend/src/views/Filters.js | 116 ------ frontend/src/views/Header.js | 109 ----- frontend/src/views/Header.scss | 34 -- frontend/src/views/Timeline.js | 232 ----------- frontend/src/views/Timeline.scss | 22 -- 57 files changed, 3015 insertions(+), 2956 deletions(-) create mode 100644 frontend/src/components/App.js delete mode 100644 frontend/src/components/Connection.js delete mode 100644 frontend/src/components/Connection.scss delete mode 100644 frontend/src/components/ConnectionContent.js delete mode 100644 frontend/src/components/ConnectionContent.scss delete mode 100644 frontend/src/components/ConnectionMatchedRules.js delete mode 100644 frontend/src/components/ConnectionMatchedRules.scss create mode 100644 frontend/src/components/Header.js create mode 100644 frontend/src/components/Header.scss delete mode 100644 frontend/src/components/MessageAction.js delete mode 100644 frontend/src/components/MessageAction.scss create mode 100644 frontend/src/components/Timeline.js create mode 100644 frontend/src/components/Timeline.scss create mode 100644 frontend/src/components/dialogs/Filters.js create mode 100644 frontend/src/components/objects/Connection.js create mode 100644 frontend/src/components/objects/Connection.scss create mode 100644 frontend/src/components/objects/ConnectionMatchedRules.js create mode 100644 frontend/src/components/objects/ConnectionMatchedRules.scss create mode 100644 frontend/src/components/objects/MessageAction.js create mode 100644 frontend/src/components/objects/MessageAction.scss create mode 100644 frontend/src/components/pages/ConfigurationPage.js create mode 100644 frontend/src/components/pages/ConfigurationPage.scss create mode 100644 frontend/src/components/pages/MainPage.js create mode 100644 frontend/src/components/pages/MainPage.scss create mode 100644 frontend/src/components/pages/ServiceUnavailablePage.js create mode 100644 frontend/src/components/pages/common.scss delete mode 100644 frontend/src/components/panels/ConfigurationPane.js delete mode 100644 frontend/src/components/panels/ConfigurationPane.scss create mode 100644 frontend/src/components/panels/ConnectionsPane.js create mode 100644 frontend/src/components/panels/ConnectionsPane.scss delete mode 100644 frontend/src/components/panels/PcapPane.js delete mode 100644 frontend/src/components/panels/PcapPane.scss create mode 100644 frontend/src/components/panels/PcapsPane.js create mode 100644 frontend/src/components/panels/PcapsPane.scss delete mode 100644 frontend/src/components/panels/RulePane.js delete mode 100644 frontend/src/components/panels/RulePane.scss create mode 100644 frontend/src/components/panels/RulesPane.js create mode 100644 frontend/src/components/panels/RulesPane.scss delete mode 100644 frontend/src/components/panels/ServicePane.js delete mode 100644 frontend/src/components/panels/ServicePane.scss create mode 100644 frontend/src/components/panels/ServicesPane.js create mode 100644 frontend/src/components/panels/ServicesPane.scss create mode 100644 frontend/src/components/panels/StreamsPane.js create mode 100644 frontend/src/components/panels/StreamsPane.scss delete mode 100644 frontend/src/views/App.js delete mode 100644 frontend/src/views/App.scss delete mode 100644 frontend/src/views/Connections.js delete mode 100644 frontend/src/views/Connections.scss delete mode 100644 frontend/src/views/Filters.js delete mode 100644 frontend/src/views/Header.js delete mode 100644 frontend/src/views/Header.scss delete mode 100644 frontend/src/views/Timeline.js delete mode 100644 frontend/src/views/Timeline.scss (limited to 'connection_streams_controller.go') diff --git a/connection_streams_controller.go b/connection_streams_controller.go index 9251a3a..89e484d 100644 --- a/connection_streams_controller.go +++ b/connection_streams_controller.go @@ -28,8 +28,7 @@ import ( ) const ( - initialPayloadsSize = 1024 - defaultQueryFormatLimit = 8024 + initialMessagesSize = 1024 initialRegexSlicesCount = 8 pwntoolsMaxServerBytes = 20 ) @@ -66,8 +65,6 @@ type RegexSlice struct { type GetMessageFormat struct { Format string `form:"format"` - Skip uint64 `form:"skip"` - Limit uint64 `form:"limit"` } type DownloadMessageFormat struct { @@ -92,12 +89,8 @@ func (csc ConnectionStreamsController) GetConnectionMessages(c context.Context, return nil, false } - payloads := make([]*Message, 0, initialPayloadsSize) - var clientIndex, serverIndex, globalIndex uint64 - - if format.Limit <= 0 { - format.Limit = defaultQueryFormatLimit - } + messages := make([]*Message, 0, initialMessagesSize) + var clientIndex, serverIndex uint64 var clientBlocksIndex, serverBlocksIndex int var clientDocumentIndex, serverDocumentIndex int @@ -111,8 +104,8 @@ func (csc ConnectionStreamsController) GetConnectionMessages(c context.Context, return serverBlocksIndex < len(serverStream.BlocksIndexes) } - var payload *Message - payloadsBuffer := make([]*Message, 0, 16) + var message *Message + messagesBuffer := make([]*Message, 0, 16) contentChunkBuffer := new(bytes.Buffer) var lastContentSlice []byte var sideChanged, lastClient, lastServer bool @@ -129,7 +122,7 @@ func (csc ConnectionStreamsController) GetConnectionMessages(c context.Context, } size := uint64(end - start) - payload = &Message{ + message = &Message{ FromClient: true, Content: DecodeBytes(clientStream.Payload[start:end], format.Format), Index: start, @@ -138,7 +131,6 @@ func (csc ConnectionStreamsController) GetConnectionMessages(c context.Context, RegexMatches: findMatchesBetween(clientStream.PatternMatches, clientIndex, clientIndex+size), } clientIndex += size - globalIndex += size clientBlocksIndex++ lastContentSlice = clientStream.Payload[start:end] @@ -153,7 +145,7 @@ func (csc ConnectionStreamsController) GetConnectionMessages(c context.Context, } size := uint64(end - start) - payload = &Message{ + message = &Message{ FromClient: false, Content: DecodeBytes(serverStream.Payload[start:end], format.Format), Index: start, @@ -162,7 +154,6 @@ func (csc ConnectionStreamsController) GetConnectionMessages(c context.Context, RegexMatches: findMatchesBetween(serverStream.PatternMatches, serverIndex, serverIndex+size), } serverIndex += size - globalIndex += size serverBlocksIndex++ lastContentSlice = serverStream.Payload[start:end] @@ -172,49 +163,43 @@ func (csc ConnectionStreamsController) GetConnectionMessages(c context.Context, 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) - } - 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, true - } + messages = append(messages, message) } - return payloads, true + return messages, true } func (csc ConnectionStreamsController) DownloadConnectionMessages(c context.Context, connectionID RowID, @@ -345,7 +330,6 @@ func findMatchesBetween(patternMatches map[uint][]PatternSlice, from, to uint64) continue } - log.Info(slice[0], slice[1], from, to) var start, end uint64 if from > slice[0] { start = 0 diff --git a/frontend/src/components/App.js b/frontend/src/components/App.js new file mode 100644 index 0000000..bf959c5 --- /dev/null +++ b/frontend/src/components/App.js @@ -0,0 +1,62 @@ +/* + * This file is part of caronte (https://github.com/eciavatta/caronte). + * Copyright (c) 2020 Emiliano Ciavatta. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import React, {Component} from 'react'; +import ConfigurationPage from "./pages/ConfigurationPage"; +import Notifications from "./Notifications"; +import dispatcher from "../dispatcher"; +import MainPage from "./pages/MainPage"; +import ServiceUnavailablePage from "./pages/ServiceUnavailablePage"; + +class App extends Component { + + state = {}; + + componentDidMount() { + dispatcher.register("notifications", payload => { + if (payload.event === "connected") { + this.setState({ + connected: true, + configured: payload.message["is_configured"] + }); + } + }); + + setInterval(() => { + if (document.title.endsWith("❚")) { + document.title = document.title.slice(0, -1); + } else { + document.title += "❚"; + } + }, 500); + } + + render() { + return ( + <> + + {this.state.connected ? + (this.state.configured ? : + this.setState({configured: true})}/>) : + + } + + ); + } +} + +export default App; diff --git a/frontend/src/components/Connection.js b/frontend/src/components/Connection.js deleted file mode 100644 index c7b0010..0000000 --- a/frontend/src/components/Connection.js +++ /dev/null @@ -1,145 +0,0 @@ -/* - * This file is part of caronte (https://github.com/eciavatta/caronte). - * Copyright (c) 2020 Emiliano Ciavatta. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -import React, {Component} from 'react'; -import './Connection.scss'; -import {Form, OverlayTrigger, Popover} from "react-bootstrap"; -import backend from "../backend"; -import {dateTimeToTime, durationBetween, formatSize} from "../utils"; -import ButtonField from "./fields/ButtonField"; -import LinkPopover from "./objects/LinkPopover"; - -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 (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 =
- Started at {startedAt.toLocaleDateString() + " " + startedAt.toLocaleTimeString()}
- Processed at {processedAt.toLocaleDateString() + " " + processedAt.toLocaleTimeString()}
- Closed at {closedAt.toLocaleDateString() + " " + closedAt.toLocaleTimeString()} -
; - - const popoverFor = function (name, content) { - return - - {content} - - ; - }; - - const commentPopoverContent =
- Click to {conn.comment.length > 0 ? "edit" : "add"} comment - {conn.comment && } -
; - - const copyPopoverContent =
- {this.state.copiedMessage ? Copied! : - Click to copy the connection id} - -
; - - return ( - 0})}> - - - this.props.addServicePortFilter(conn["port_dst"])}/> - - - {conn["ip_src"]} - {conn["port_src"]} - {conn["ip_dst"]} - {conn["port_dst"]} - - - - {durationBetween(startedAt, closedAt)} - {formatSize(conn["client_bytes"])} - {formatSize(conn["server_bytes"])} - - Mark this connection)}> - this.handleAction("mark")}>!! - - - this.handleAction("comment")}>@ - - - this.handleAction("copy")}># - - - - ); - } - -} - -export default Connection; diff --git a/frontend/src/components/Connection.scss b/frontend/src/components/Connection.scss deleted file mode 100644 index cc1ea96..0000000 --- a/frontend/src/components/Connection.scss +++ /dev/null @@ -1,68 +0,0 @@ -@import "../colors.scss"; - -.connection { - border-top: 3px solid $color-primary-3; - border-bottom: 3px solid $color-primary-3; - background-color: $color-primary-0; - - td { - vertical-align: middle; - } - - .connection-service { - .btn { - width: 100%; - } - - .btn:hover { - color: $color-primary-4; - background-color: $color-primary-1 !important; - } - } - - .connection-icon { - font-size: 18px; - font-weight: 600; - margin-right: 6px; - cursor: pointer; - } - - .connection-icon.icon-enabled { - color: $color-secondary-2; - } - - &:hover { - background-color: $color-primary-2; - } - - &.connection-selected { - background-color: $color-primary-2; - } - - &.has-matched-rules { - border-bottom: 0; - } - - .link-popover { - font-weight: 400; - } -} - -.connection-popover { - border: none; - background-color: $color-primary-4; - - .popover-body { - color: $color-primary-1; - - textarea { - font-size: 12px; - width: 200px; - background-color: $color-primary-3; - } - } - - .arrow::after { - border-right-color: $color-primary-4; - } -} diff --git a/frontend/src/components/ConnectionContent.js b/frontend/src/components/ConnectionContent.js deleted file mode 100644 index b468277..0000000 --- a/frontend/src/components/ConnectionContent.js +++ /dev/null @@ -1,243 +0,0 @@ -/* - * This file is part of caronte (https://github.com/eciavatta/caronte). - * Copyright (c) 2020 Emiliano Ciavatta. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -import React, {Component} from 'react'; -import './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"; -import DOMPurify from 'dompurify'; -import ReactJson from 'react-json-view' -import {downloadBlob, getHeaderValue} from "../utils"; -import log from "../log"; - -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"]; - } - - componentDidMount() { - if (this.props.connection != null) { - this.loadStream(); - } - - document.title = "caronte:~/$"; - } - - componentDidUpdate(prevProps, prevState, snapshot) { - if (this.props.connection != null && ( - this.props.connection !== prevProps.connection || this.state.format !== prevState.format)) { - this.closeRenderWindow(); - this.loadStream(); - } - } - - componentWillUnmount() { - this.closeRenderWindow(); - } - - 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 **already parsed in previous messages**; - } - - let unrollMap = (obj) => obj == null ? null : Object.entries(obj).map(([key, value]) => -

{key}: {value}

- ); - - let m = connectionMessage.metadata; - switch (m.type) { - case "http-request": - let url = {m.host}{m.url}; - return -

{m.method} {url} {m.protocol}

- {unrollMap(m.headers)} -
{m.body}
- {unrollMap(m.trailers)} -
; - case "http-response": - const contentType = getHeaderValue(m, "Content-Type"); - let body = m.body; - if (contentType && contentType.includes("application/json")) { - try { - const json = JSON.parse(m.body); - body = ; - } catch (e) { - console.log(e); - } - } - - return -

{m.protocol} {m.status}

- {unrollMap(m.headers)} -
{body}
- {unrollMap(m.trailers)} -
; - default: - return connectionMessage.content; - } - }; - - connectionsActions = (connectionMessage) => { - if (connectionMessage.metadata == null) { //} || !connectionMessage.metadata["reproducers"]) { - return null; - } - - const m = connectionMessage.metadata; - switch (m.type) { - case "http-request" : - if (!connectionMessage.metadata["reproducers"]) { - return; - } - return Object.entries(connectionMessage.metadata["reproducers"]).map(([actionName, actionValue]) => - { - this.setState({ - messageActionDialog: this.setState({messageActionDialog: null})}/> - }); - }}/> - ); - case "http-response": - const contentType = getHeaderValue(m, "Content-Type"); - - if (contentType && contentType.includes("text/html")) { - return { - let w; - if (this.state.renderWindow && !this.state.renderWindow.closed) { - w = this.state.renderWindow; - } else { - w = window.open("", "", "width=900, height=600, scrollbars=yes"); - this.setState({renderWindow: w}); - } - w.document.body.innerHTML = DOMPurify.sanitize(m.body); - w.focus(); - }}/>; - } - break; - default: - return null; - } - }; - - downloadStreamRaw = (value) => { - backend.download(`/api/streams/${this.props.connection.id}/download?format=${this.state.format}&type=${value}`) - .then(res => downloadBlob(res.blob, `${this.props.connection.id}-${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; - const content = this.state.connectionContent; - - if (content == null) { - return
select a connection to view
; - } - - let payload = content.map((c, i) => -
-
-
-
- offset: {c.index} | timestamp: {c.timestamp} - | retransmitted: {c["is_retransmitted"] ? "yes" : "no"} -
-
{this.connectionsActions(c)}
-
-
-
{c["from_client"] ? "client" : "server"}
-
- {this.state.tryParse && this.state.format === "default" ? this.tryParseConnectionMessage(c) : c.content} -
-
- ); - - return ( -
-
- -
- flow: {conn["ip_src"]}:{conn["port_src"]} -> {conn["ip_dst"]}:{conn["port_dst"]} - | timestamp: {conn["started_at"]} -
-
- - - - - -
-
-
- -
{payload}
- {this.state.messageActionDialog} -
- ); - } - -} - - -export default ConnectionContent; diff --git a/frontend/src/components/ConnectionContent.scss b/frontend/src/components/ConnectionContent.scss deleted file mode 100644 index f4edec9..0000000 --- a/frontend/src/components/ConnectionContent.scss +++ /dev/null @@ -1,113 +0,0 @@ -@import "../colors.scss"; - -.connection-content { - height: 100%; - background-color: $color-primary-0; - - pre { - overflow-x: hidden; - height: calc(100% - 31px); - padding: 0 10px; - white-space: pre-wrap; - word-break: break-word; - - p { - margin: 0; - padding: 0; - } - } - - .connection-message { - position: relative; - margin: 10px 0; - border: 4px solid $color-primary-3; - border-top: 0; - - .connection-message-header { - height: 25px; - background-color: $color-primary-3; - - .connection-message-info { - font-size: 11px; - margin-top: 6px; - margin-left: -10px; - } - - .connection-message-actions { - display: none; - margin-right: -18px; - - button { - font-size: 11px; - margin: 0 3px; - padding: 5px; - } - } - } - - .message-content { - padding: 10px; - - .react-json-view { - background-color: inherit !important; - } - } - - &:hover .connection-message-actions { - display: flex; - } - - .connection-message-label { - font-size: 12px; - position: absolute; - top: 0; - padding: 10px 0; - background-color: $color-primary-3; - writing-mode: vertical-rl; - text-orientation: mixed; - } - - &.from-client { - margin-right: 100px; - color: $color-primary-4; - - .connection-message-label { - right: -22px; - } - } - - &.from-server { - margin-left: 100px; - color: $color-primary-4; - - .connection-message-label { - left: -22px; - transform: rotate(-180deg); - } - } - } - - .connection-content-header { - height: 33px; - padding: 0; - background-color: $color-primary-3; - - .header-info { - font-size: 12px; - padding-top: 7px; - padding-left: 25px; - } - - .header-actions { - display: flex; - - .choice-field { - margin-top: -5px; - - .field-value { - background-color: $color-primary-3; - } - } - } - } -} diff --git a/frontend/src/components/ConnectionMatchedRules.js b/frontend/src/components/ConnectionMatchedRules.js deleted file mode 100644 index 35643c5..0000000 --- a/frontend/src/components/ConnectionMatchedRules.js +++ /dev/null @@ -1,46 +0,0 @@ -/* - * This file is part of caronte (https://github.com/eciavatta/caronte). - * Copyright (c) 2020 Emiliano Ciavatta. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -import React, {Component} from 'react'; -import './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 this.props.addMatchedRulesFilter(rule.id)} name={rule.name} - color={rule.color} small />; - }); - - return ( - - matched_rules: - {matchedRules} - - ); - } -} - -export default ConnectionMatchedRules; diff --git a/frontend/src/components/ConnectionMatchedRules.scss b/frontend/src/components/ConnectionMatchedRules.scss deleted file mode 100644 index 65d9ac8..0000000 --- a/frontend/src/components/ConnectionMatchedRules.scss +++ /dev/null @@ -1,23 +0,0 @@ -@import "../colors.scss"; - -.connection-matches { - background-color: $color-primary-0; - - .rule-buttons { - padding: 0; - } - - .button-field { - display: inline-block; - margin-right: 5px; - - button { - font-size: 0.8em; - padding: 2px 10px; - } - } - - .row-label { - font-size: 0.8em; - } -} diff --git a/frontend/src/components/Header.js b/frontend/src/components/Header.js new file mode 100644 index 0000000..4d29364 --- /dev/null +++ b/frontend/src/components/Header.js @@ -0,0 +1,111 @@ +/* + * This file is part of caronte (https://github.com/eciavatta/caronte). + * Copyright (c) 2020 Emiliano Ciavatta. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import React, {Component} from 'react'; +import Typed from 'typed.js'; +import './Header.scss'; +import {filtersDefinitions, filtersNames} from "./filters/FiltersDefinitions"; +import {Link, withRouter} from "react-router-dom"; +import ButtonField from "./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 => {filtersDefinitions[name]}) + .slice(0, 5); + + return ( +
+
+
+

+ + { + this.el = el; + }}/> + +

+
+ +
+
+ {quickFilters} +
+
+ +
+
+ + + + + + + + + + + + + +
+
+
+
+ ); + } +} + +export default withRouter(Header); diff --git a/frontend/src/components/Header.scss b/frontend/src/components/Header.scss new file mode 100644 index 0000000..e2e8e1c --- /dev/null +++ b/frontend/src/components/Header.scss @@ -0,0 +1,34 @@ +@import "../colors"; + +.header { + height: 80px; + padding: 15px 30px; + + > .row { + background-color: $color-primary-0; + } + + .header-title { + width: 200px; + margin: 5px 0 5px -5px; + } + + .header-buttons { + display: flex; + justify-content: flex-end; + margin: 7px 0; + + .button-field { + margin-left: 7px; + } + } + + .filters-bar { + padding: 3px 0; + + .filter { + display: inline-block; + margin-right: 10px; + } + } +} diff --git a/frontend/src/components/MessageAction.js b/frontend/src/components/MessageAction.js deleted file mode 100644 index b94cbb9..0000000 --- a/frontend/src/components/MessageAction.js +++ /dev/null @@ -1,68 +0,0 @@ -/* - * This file is part of caronte (https://github.com/eciavatta/caronte). - * Copyright (c) 2020 Emiliano Ciavatta. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -import React, {Component} from 'react'; -import './MessageAction.scss'; -import {Modal} from "react-bootstrap"; -import TextField from "./fields/TextField"; -import ButtonField from "./fields/ButtonField"; - -class MessageAction extends Component { - - constructor(props) { - super(props); - this.state = { - copyButtonText: "copy" - }; - this.actionValue = React.createRef(); - this.copyActionValue = this.copyActionValue.bind(this); - } - - copyActionValue() { - this.actionValue.current.select(); - document.execCommand('copy'); - this.setState({copyButtonText: "copied!"}); - setTimeout(() => this.setState({copyButtonText: "copy"}), 3000); - } - - render() { - return ( - - - - {this.props.actionName} - - - - - - - - - - - ); - } -} - -export default MessageAction; diff --git a/frontend/src/components/MessageAction.scss b/frontend/src/components/MessageAction.scss deleted file mode 100644 index faa23d3..0000000 --- a/frontend/src/components/MessageAction.scss +++ /dev/null @@ -1,8 +0,0 @@ -@import "../colors.scss"; - -.message-action-value { - font-size: 13px; - padding: 15px; - color: $color-primary-4; - background-color: $color-primary-2; -} diff --git a/frontend/src/components/Timeline.js b/frontend/src/components/Timeline.js new file mode 100644 index 0000000..7be42e0 --- /dev/null +++ b/frontend/src/components/Timeline.js @@ -0,0 +1,232 @@ +/* + * This file is part of caronte (https://github.com/eciavatta/caronte). + * Copyright (c) 2020 Emiliano Ciavatta. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import React, {Component} from 'react'; +import './Timeline.scss'; +import { + ChartContainer, + ChartRow, + Charts, + LineChart, + MultiBrush, + Resizable, + styler, + YAxis +} from "react-timeseries-charts"; +import {TimeRange, TimeSeries} from "pondjs"; +import backend from "../backend"; +import ChoiceField from "./fields/ChoiceField"; +import {withRouter} from "react-router-dom"; +import log from "../log"; +import dispatcher from "../dispatcher"; + +const minutes = 60 * 1000; + +class Timeline extends Component { + + state = { + metric: "connections_per_service" + }; + + constructor() { + super(); + + this.disableTimeSeriesChanges = false; + this.selectionTimeout = null; + } + + filteredPort = () => { + const urlParams = new URLSearchParams(this.props.location.search); + return urlParams.get("service_port"); + }; + + componentDidMount() { + const filteredPort = this.filteredPort(); + this.setState({filteredPort}); + this.loadStatistics(this.state.metric, filteredPort).then(() => log.debug("Statistics loaded after mount")); + + dispatcher.register("connection_updates", payload => { + this.setState({ + selection: new TimeRange(payload.from, payload.to), + }); + }); + + dispatcher.register("notifications", payload => { + if (payload.event === "services.edit") { + this.loadServices().then(() => log.debug("Services reloaded after notification update")); + } + }); + } + + componentDidUpdate(prevProps, prevState, snapshot) { + const filteredPort = this.filteredPort(); + if (this.state.filteredPort !== filteredPort) { + this.setState({filteredPort}); + this.loadStatistics(this.state.metric, filteredPort).then(() => + log.debug("Statistics reloaded after filtered port changes")); + } + } + + loadStatistics = async (metric, filteredPort) => { + const urlParams = new URLSearchParams(); + urlParams.set("metric", metric); + + let services = await this.loadServices(); + if (filteredPort && services[filteredPort]) { + const service = services[filteredPort]; + services = {}; + services[filteredPort] = service; + } + + const ports = Object.keys(services); + ports.forEach(s => urlParams.append("ports", s)); + + const metrics = (await backend.get("/api/statistics?" + urlParams)).json; + const zeroFilledMetrics = []; + const toTime = m => new Date(m["range_start"]).getTime(); + + if (metrics.length > 0) { + let i = 0; + for (let interval = toTime(metrics[0]); interval <= toTime(metrics[metrics.length - 1]); interval += minutes) { + if (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] = {}; + ports.forEach(p => m[metric][p] = 0); + zeroFilledMetrics.push(m); + } + } + } + + const series = new TimeSeries({ + name: "statistics", + columns: ["time"].concat(ports), + points: zeroFilledMetrics.map(m => [m["range_start"]].concat(ports.map(p => m[metric][p] || 0))) + }); + const start = series.range().begin(); + const end = series.range().end(); + start.setTime(start.getTime() - minutes); + end.setTime(end.getTime() + minutes); + + this.setState({ + metric, + series, + timeRange: new TimeRange(start, end), + start, + end + }); + log.debug(`Loaded statistics for metric "${metric}" for services [${ports}]`); + }; + + loadServices = async () => { + const services = (await backend.get("/api/services")).json; + this.setState({services}); + return services; + }; + + createStyler = () => { + 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); + }; + + 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 ( +
+
+ + + + + + + + + + + + + + +
+ this.loadStatistics(metric, this.state.filteredPort) + .then(() => log.debug("Statistics loaded after metric changes"))} + value={this.state.metric}/> +
+
+
+ ); + } +} + +export default withRouter(Timeline); diff --git a/frontend/src/components/Timeline.scss b/frontend/src/components/Timeline.scss new file mode 100644 index 0000000..eeb9d50 --- /dev/null +++ b/frontend/src/components/Timeline.scss @@ -0,0 +1,22 @@ +@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; + } + } + + 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..35c11df --- /dev/null +++ b/frontend/src/components/dialogs/Filters.js @@ -0,0 +1,116 @@ +/* + * This file is part of caronte (https://github.com/eciavatta/caronte). + * Copyright (c) 2020 Emiliano Ciavatta. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import React, {Component} from 'react'; +import {Col, Container, Modal, Row, Table} from "react-bootstrap"; +import {filtersDefinitions, filtersNames} from "../filters/FiltersDefinitions"; +import ButtonField from "../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 => + + this.checkboxChangesHandler(name, event)}/> + {filtersDefinitions[name]} + + ); + } + + render() { + return ( + + + + ~/filters + + + + + + + + + + + + + + + {this.generateRows(["service_port", "client_address", "min_duration", + "min_bytes", "started_after", "closed_after", "marked"])} + +
showfilter
+ + + + + + + + + + + {this.generateRows(["matched_rules", "client_port", "max_duration", + "max_bytes", "started_before", "closed_before", "hidden"])} + +
showfilter
+ + +
+ + +
+
+ + + +
+ ); + } +} + +export default Filters; diff --git a/frontend/src/components/objects/Connection.js b/frontend/src/components/objects/Connection.js new file mode 100644 index 0000000..5e2beba --- /dev/null +++ b/frontend/src/components/objects/Connection.js @@ -0,0 +1,145 @@ +/* + * This file is part of caronte (https://github.com/eciavatta/caronte). + * Copyright (c) 2020 Emiliano Ciavatta. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import React, {Component} from 'react'; +import './Connection.scss'; +import {Form, OverlayTrigger, Popover} from "react-bootstrap"; +import backend from "../../backend"; +import {dateTimeToTime, durationBetween, formatSize} from "../../utils"; +import ButtonField from "../fields/ButtonField"; +import LinkPopover from "./LinkPopover"; + +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 (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 =
+ Started at {startedAt.toLocaleDateString() + " " + startedAt.toLocaleTimeString()}
+ Processed at {processedAt.toLocaleDateString() + " " + processedAt.toLocaleTimeString()}
+ Closed at {closedAt.toLocaleDateString() + " " + closedAt.toLocaleTimeString()} +
; + + const popoverFor = function (name, content) { + return + + {content} + + ; + }; + + const commentPopoverContent =
+ Click to {conn.comment.length > 0 ? "edit" : "add"} comment + {conn.comment && } +
; + + const copyPopoverContent =
+ {this.state.copiedMessage ? Copied! : + Click to copy the connection id} + +
; + + return ( + 0})}> + + + this.props.addServicePortFilter(conn["port_dst"])}/> + + + {conn["ip_src"]} + {conn["port_src"]} + {conn["ip_dst"]} + {conn["port_dst"]} + + + + {durationBetween(startedAt, closedAt)} + {formatSize(conn["client_bytes"])} + {formatSize(conn["server_bytes"])} + + Mark this connection)}> + this.handleAction("mark")}>!! + + + this.handleAction("comment")}>@ + + + this.handleAction("copy")}># + + + + ); + } + +} + +export default Connection; diff --git a/frontend/src/components/objects/Connection.scss b/frontend/src/components/objects/Connection.scss new file mode 100644 index 0000000..3b9f479 --- /dev/null +++ b/frontend/src/components/objects/Connection.scss @@ -0,0 +1,68 @@ +@import "../../colors"; + +.connection { + border-top: 3px solid $color-primary-3; + border-bottom: 3px solid $color-primary-3; + background-color: $color-primary-0; + + td { + vertical-align: middle; + } + + .connection-service { + .btn { + width: 100%; + } + + .btn:hover { + color: $color-primary-4; + background-color: $color-primary-1 !important; + } + } + + .connection-icon { + font-size: 18px; + font-weight: 600; + margin-right: 6px; + cursor: pointer; + } + + .connection-icon.icon-enabled { + color: $color-secondary-2; + } + + &:hover { + background-color: $color-primary-2; + } + + &.connection-selected { + background-color: $color-primary-2; + } + + &.has-matched-rules { + border-bottom: 0; + } + + .link-popover { + font-weight: 400; + } +} + +.connection-popover { + border: none; + background-color: $color-primary-4; + + .popover-body { + color: $color-primary-1; + + textarea { + font-size: 12px; + width: 200px; + background-color: $color-primary-3; + } + } + + .arrow::after { + border-right-color: $color-primary-4; + } +} diff --git a/frontend/src/components/objects/ConnectionMatchedRules.js b/frontend/src/components/objects/ConnectionMatchedRules.js new file mode 100644 index 0000000..73d5c5d --- /dev/null +++ b/frontend/src/components/objects/ConnectionMatchedRules.js @@ -0,0 +1,46 @@ +/* + * This file is part of caronte (https://github.com/eciavatta/caronte). + * Copyright (c) 2020 Emiliano Ciavatta. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import React, {Component} from 'react'; +import './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 this.props.addMatchedRulesFilter(rule.id)} name={rule.name} + color={rule.color} small />; + }); + + return ( + + matched_rules: + {matchedRules} + + ); + } +} + +export default ConnectionMatchedRules; diff --git a/frontend/src/components/objects/ConnectionMatchedRules.scss b/frontend/src/components/objects/ConnectionMatchedRules.scss new file mode 100644 index 0000000..f46a914 --- /dev/null +++ b/frontend/src/components/objects/ConnectionMatchedRules.scss @@ -0,0 +1,23 @@ +@import "../../colors"; + +.connection-matches { + background-color: $color-primary-0; + + .rule-buttons { + padding: 0; + } + + .button-field { + display: inline-block; + margin-right: 5px; + + button { + font-size: 0.8em; + padding: 2px 10px; + } + } + + .row-label { + font-size: 0.8em; + } +} diff --git a/frontend/src/components/objects/MessageAction.js b/frontend/src/components/objects/MessageAction.js new file mode 100644 index 0000000..9f199b7 --- /dev/null +++ b/frontend/src/components/objects/MessageAction.js @@ -0,0 +1,68 @@ +/* + * This file is part of caronte (https://github.com/eciavatta/caronte). + * Copyright (c) 2020 Emiliano Ciavatta. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import React, {Component} from 'react'; +import './MessageAction.scss'; +import {Modal} from "react-bootstrap"; +import TextField from "../fields/TextField"; +import ButtonField from "../fields/ButtonField"; + +class MessageAction extends Component { + + constructor(props) { + super(props); + this.state = { + copyButtonText: "copy" + }; + this.actionValue = React.createRef(); + this.copyActionValue = this.copyActionValue.bind(this); + } + + copyActionValue() { + this.actionValue.current.select(); + document.execCommand('copy'); + this.setState({copyButtonText: "copied!"}); + setTimeout(() => this.setState({copyButtonText: "copy"}), 3000); + } + + render() { + return ( + + + + {this.props.actionName} + + + + + + + + + + + ); + } +} + +export default MessageAction; diff --git a/frontend/src/components/objects/MessageAction.scss b/frontend/src/components/objects/MessageAction.scss new file mode 100644 index 0000000..996007b --- /dev/null +++ b/frontend/src/components/objects/MessageAction.scss @@ -0,0 +1,8 @@ +@import "../../colors"; + +.message-action-value { + font-size: 13px; + padding: 15px; + color: $color-primary-4; + background-color: $color-primary-2; +} diff --git a/frontend/src/components/pages/ConfigurationPage.js b/frontend/src/components/pages/ConfigurationPage.js new file mode 100644 index 0000000..6ab8ae3 --- /dev/null +++ b/frontend/src/components/pages/ConfigurationPage.js @@ -0,0 +1,179 @@ +/* + * This file is part of caronte (https://github.com/eciavatta/caronte). + * Copyright (c) 2020 Emiliano Ciavatta. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import React, {Component} from 'react'; +import '../panels/common.scss'; +import './ConfigurationPage.scss'; +import LinkPopover from "../objects/LinkPopover"; +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"; + +class ConfigurationPage extends Component { + + constructor(props) { + super(props); + this.state = { + settings: { + "config": { + "server_address": "", + "flag_regex": "", + "auth_required": false + }, + "accounts": { + } + }, + newUsername: "", + newPassword: "" + }; + } + + saveSettings = () => { + if (this.validateSettings(this.state.settings)) { + backend.post("/setup", this.state.settings).then(_ => { + this.props.onConfigured(); + }).catch(res => { + this.setState({setupStatusCode: res.status, setupResponse: JSON.stringify(res.json)}); + }); + } + }; + + validateSettings = (settings) => { + let valid = true; + if (!validation.isValidAddress(settings.config.server_address, true)) { + this.setState({serverAddressError: "invalid ip_address"}); + valid = false; + } + if (settings.config.flag_regex.length < 8) { + this.setState({flagRegexError: "flag_regex.length < 8"}); + valid = false; + } + + return valid; + }; + + updateParam = (callback) => { + callback(this.state.settings); + this.setState({settings: this.state.settings}); + }; + + addAccount = () => { + if (this.state.newUsername.length !== 0 && this.state.newPassword.length !== 0) { + const settings = this.state.settings; + settings.accounts[this.state.newUsername] = this.state.newPassword; + + this.setState({ + newUsername: "", + newPassword: "", + settings: settings + }); + } else { + this.setState({ + newUsernameActive: this.state.newUsername.length === 0, + newPasswordActive: this.state.newPassword.length === 0 + }); + } + }; + + render() { + const settings = this.state.settings; + const curlCommand = createCurlCommand("/setup", "POST", settings); + + const accounts = Object.entries(settings.accounts).map(([username, password]) => + + {username} + + this.updateParam((s) => delete s.accounts[username]) }/> + ).concat( + this.setState({newUsername: v})} /> + this.setState({newPassword: v})} /> + + ); + + return ( +
+
+
+
+
+ POST /setup + +
+ +
+ + + + this.updateParam((s) => s.config.server_address = v)} /> + this.updateParam((s) => s.config.flag_regex = v)} + error={this.state.flagRegexError} /> +
+ this.updateParam((s) => s.config.auth_required = v)}/> +
+ + + + + accounts: +
+ + + + + + + + + + {accounts} + +
usernamepasswordactions
+
+ +
+
+ + +
+ +
+ +
+
+
+
+
+ ); + } +} + +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..4509865 --- /dev/null +++ b/frontend/src/components/pages/ConfigurationPage.scss @@ -0,0 +1,18 @@ +@import "../../colors"; + +.configuration-page { + 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/pages/MainPage.js b/frontend/src/components/pages/MainPage.js new file mode 100644 index 0000000..7376091 --- /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 . + */ + +import React, {Component} from 'react'; +import './MainPage.scss'; +import './common.scss'; +import Connections from "../panels/ConnectionsPane"; +import StreamsPane from "../panels/StreamsPane"; +import {BrowserRouter as Router, Route, Switch} from "react-router-dom"; +import Timeline from "../Timeline"; +import PcapsPane from "../panels/PcapsPane"; +import RulesPane from "../panels/RulesPane"; +import ServicesPane from "../panels/ServicesPane"; +import Header from "../Header"; +import Filters from "../dialogs/Filters"; +import MainPane from "../panels/MainPane"; + +class MainPage extends Component { + + state = {}; + + render() { + let modal; + if (this.state.filterWindowOpen) { + modal = this.setState({filterWindowOpen: false})}/>; + } + + return ( +
+ +
+
this.setState({filterWindowOpen: true})}/> +
+ +
+
+ this.setState({selectedConnection: c})}/> +
+
+ + }/> + }/> + }/> + }/> + }/> + +
+ + {modal} +
+ +
+ +
+
+
+ ); + } +} + +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..3b1a689 --- /dev/null +++ b/frontend/src/components/pages/MainPage.scss @@ -0,0 +1,24 @@ +@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 { + 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..f27d84d --- /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 . + */ + +import React, {Component} from 'react'; +import './MainPage.scss'; + +class ServiceUnavailablePage extends Component { + + state = {}; + + render() { + return ( +
+ +
+ ); + } +} + +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.js b/frontend/src/components/panels/ConfigurationPane.js deleted file mode 100644 index 9ae2cfb..0000000 --- a/frontend/src/components/panels/ConfigurationPane.js +++ /dev/null @@ -1,179 +0,0 @@ -/* - * This file is part of caronte (https://github.com/eciavatta/caronte). - * Copyright (c) 2020 Emiliano Ciavatta. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -import React, {Component} from 'react'; -import './common.scss'; -import './ConfigurationPane.scss'; -import LinkPopover from "../objects/LinkPopover"; -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"; - -class ConfigurationPane extends Component { - - constructor(props) { - super(props); - this.state = { - settings: { - "config": { - "server_address": "", - "flag_regex": "", - "auth_required": false - }, - "accounts": { - } - }, - newUsername: "", - newPassword: "" - }; - } - - saveSettings = () => { - if (this.validateSettings(this.state.settings)) { - backend.post("/setup", this.state.settings).then(_ => { - this.props.onConfigured(); - }).catch(res => { - this.setState({setupStatusCode: res.status, setupResponse: JSON.stringify(res.json)}); - }); - } - }; - - validateSettings = (settings) => { - let valid = true; - if (!validation.isValidAddress(settings.config.server_address, true)) { - this.setState({serverAddressError: "invalid ip_address"}); - valid = false; - } - if (settings.config.flag_regex.length < 8) { - this.setState({flagRegexError: "flag_regex.length < 8"}); - valid = false; - } - - return valid; - }; - - updateParam = (callback) => { - callback(this.state.settings); - this.setState({settings: this.state.settings}); - }; - - addAccount = () => { - if (this.state.newUsername.length !== 0 && this.state.newPassword.length !== 0) { - const settings = this.state.settings; - settings.accounts[this.state.newUsername] = this.state.newPassword; - - this.setState({ - newUsername: "", - newPassword: "", - settings: settings - }); - } else { - this.setState({ - newUsernameActive: this.state.newUsername.length === 0, - newPasswordActive: this.state.newPassword.length === 0 - }); - } - }; - - render() { - const settings = this.state.settings; - const curlCommand = createCurlCommand("/setup", "POST", settings); - - const accounts = Object.entries(settings.accounts).map(([username, password]) => - - {username} - - this.updateParam((s) => delete s.accounts[username]) }/> - ).concat( - this.setState({newUsername: v})} /> - this.setState({newPassword: v})} /> - - ); - - return ( -
-
-
-
-
- POST /setup - -
- -
- - - - this.updateParam((s) => s.config.server_address = v)} /> - this.updateParam((s) => s.config.flag_regex = v)} - error={this.state.flagRegexError} /> -
- this.updateParam((s) => s.config.auth_required = v)}/> -
- - - - - accounts: -
- - - - - - - - - - {accounts} - -
usernamepasswordactions
-
- -
-
- - -
- -
- -
-
-
-
-
- ); - } -} - -export default ConfigurationPane; 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..038ef8f --- /dev/null +++ b/frontend/src/components/panels/ConnectionsPane.js @@ -0,0 +1,304 @@ +/* + * This file is part of caronte (https://github.com/eciavatta/caronte). + * Copyright (c) 2020 Emiliano Ciavatta. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import React, {Component} from 'react'; +import './ConnectionsPane.scss'; +import Connection from "../objects/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 "../objects/ConnectionMatchedRules"; +import log from "../../log"; +import ButtonField from "../fields/ButtonField"; +import dispatcher from "../../dispatcher"; + +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() { + const initialParams = {limit: this.queryLimit}; + + const match = this.props.location.pathname.match(/^\/connections\/([a-f0-9]{24})$/); + if (match != null) { + const id = match[1]; + initialParams.from = id; + backend.get(`/api/connections/${id}`) + .then(res => this.connectionSelected(res.json, false)) + .catch(error => log.error("Error loading initial connection", error)); + } + + this.loadConnections(initialParams, true).then(() => log.debug("Connections loaded")); + + dispatcher.register("timeline_updates", 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}`)); + }); + + dispatcher.register("notifications", payload => { + if (payload.event === "rules.new" || payload.event === "rules.edit") { + this.loadRules().then(() => log.debug("Loaded connection rules after notification update")); + } + }); + + dispatcher.register("notifications", payload => { + if (payload.event === "services.edit") { + this.loadServices().then(() => log.debug("Services reloaded after notification update")); + } + }); + } + + connectionSelected = (c, doRedirect = true) => { + this.doSelectedConnectionRedirect = doRedirect; + this.setState({selected: c.id}); + this.props.onSelected(c); + log.debug(`Connection ${c.id} selected`); + }; + + componentDidUpdate(prevProps, prevState, snapshot) { + if (prevProps.location.search !== this.props.location.search) { + this.loadConnections({limit: this.queryLimit}) + .then(() => log.info("ConnectionsPane reloaded after query string update")); + } + } + + 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; + }; + + addServicePortFilter = (port) => { + const urlParams = new URLSearchParams(this.props.location.search); + urlParams.set("service_port", port); + this.doQueryStringRedirect = true; + 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.doQueryStringRedirect = true; + this.setState({queryString: urlParams}); + } + }; + + async loadConnections(params, isInitial = false) { + const urlParams = new URLSearchParams(this.props.location.search); + for (const [name, value] of Object.entries(params)) { + 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 (params !== undefined && params.from !== undefined && params.to === undefined) { + 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 (params !== undefined && params.to !== undefined && params.from === undefined) { + 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: connections, + firstConnection: firstConnection, + lastConnection: 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.doSelectedConnectionRedirect) { + redirect = ; + this.doSelectedConnectionRedirect = false; + } else if (this.doQueryStringRedirect) { + redirect = ; + this.doQueryStringRedirect = false; + } + + let loading = null; + if (this.state.loading) { + loading = + Loading... + ; + } + + return ( +
+ {this.state.showMoreRecentButton &&
+ { + this.disableScrollHandler = true; + this.connectionsListRef.current.scrollTop = 0; + this.loadConnections({limit: this.queryLimit}) + .then(() => { + this.disableScrollHandler = false; + log.info("Most recent connections loaded"); + }); + }}/> +
} + +
+ + + + + + + + + + + + + + + + + { + this.state.connections.flatMap(c => { + return [ this.connectionSelected(c)} + selected={this.state.selected === c.id} + onMarked={marked => c.marked = marked} + onEnabled={enabled => c.hidden = !enabled} + addServicePortFilter={this.addServicePortFilter} + services={this.state.services}/>, + c.matched_rules.length > 0 && + + ]; + }) + } + {loading} + +
servicesrcipsrcportdstipdstportstarted_atdurationupdownactions
+ + {redirect} +
+
+ ); + } + +} + +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..06f5827 --- /dev/null +++ b/frontend/src/components/panels/ConnectionsPane.scss @@ -0,0 +1,38 @@ +@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); + background-color: red; + } +} diff --git a/frontend/src/components/panels/MainPane.js b/frontend/src/components/panels/MainPane.js index d34d58a..74c859c 100644 --- a/frontend/src/components/panels/MainPane.js +++ b/frontend/src/components/panels/MainPane.js @@ -17,57 +17,22 @@ 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"; -import log from "../../log"; +import './ServicesPane.scss'; class MainPane extends Component { state = {}; - componentDidMount() { - const match = this.props.location.pathname.match(/^\/connections\/([a-f0-9]{24})$/); - if (match != null) { - this.loading = true; - backend.get(`/api/connections/${match[1]}`) - .then(res => { - this.loading = false; - this.setState({selectedConnection: res.json}); - log.debug(`Initial connection ${match[1]} loaded`); - }) - .catch(error => log.error("Error loading initial connection", error)); - } - } - render() { return ( -
-
- { - !this.loading && - this.setState({selectedConnection: c})} - initialConnection={this.state.selectedConnection}/> - } -
-
- - }/> - }/> - }/> - }/> - }/> - +
+
+ MainPane
); } + } -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..c8460f2 100644 --- a/frontend/src/components/panels/MainPane.scss +++ b/frontend/src/components/panels/MainPane.scss @@ -1,22 +1,5 @@ @import "../../colors"; .main-pane { - display: flex; - height: 100%; - padding: 0 15px; - background-color: $color-primary-2; - .pane { - flex: 1; - } - - .connections-pane { - flex: 1 0; - margin-right: 7.5px; - } - - .details-pane { - flex: 1 1; - margin-left: 7.5px; - } } diff --git a/frontend/src/components/panels/PcapPane.js b/frontend/src/components/panels/PcapPane.js deleted file mode 100644 index d5c2225..0000000 --- a/frontend/src/components/panels/PcapPane.js +++ /dev/null @@ -1,273 +0,0 @@ -/* - * This file is part of caronte (https://github.com/eciavatta/caronte). - * Copyright (c) 2020 Emiliano Ciavatta. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -import React, {Component} from 'react'; -import './PcapPane.scss'; -import './common.scss'; -import Table from "react-bootstrap/Table"; -import backend from "../../backend"; -import {createCurlCommand, dateTimeToTime, durationBetween, formatSize} from "../../utils"; -import InputField from "../fields/InputField"; -import CheckField from "../fields/CheckField"; -import TextField from "../fields/TextField"; -import ButtonField from "../fields/ButtonField"; -import LinkPopover from "../objects/LinkPopover"; -import dispatcher from "../../dispatcher"; - -class PcapPane extends Component { - - state = { - sessions: [], - isUploadFileValid: true, - isUploadFileFocused: false, - uploadFlushAll: false, - isFileValid: true, - isFileFocused: false, - fileValue: "", - processFlushAll: false, - deleteOriginalFile: false - }; - - componentDidMount() { - this.loadSessions(); - - dispatcher.register("notifications", payload => { - if (payload.event === "pcap.upload" || payload.event === "pcap.file") { - this.loadSessions(); - } - }); - - document.title = "caronte:~/pcaps$"; - } - - loadSessions = () => { - backend.get("/api/pcap/sessions") - .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) - })); - }; - - uploadPcap = () => { - if (this.state.uploadSelectedFile == null || !this.state.isUploadFileValid) { - this.setState({isUploadFileFocused: true}); - return; - } - - 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 => { - this.setState({ - uploadStatusCode: res.status, - uploadResponse: JSON.stringify(res.json) - }); - this.resetUpload(); - this.loadSessions(); - }).catch(res => this.setState({ - uploadStatusCode: res.status, - uploadResponse: JSON.stringify(res.json) - }) - ); - }; - - processPcap = () => { - if (this.state.fileValue === "" || !this.state.isFileValid) { - this.setState({isFileFocused: true}); - return; - } - - backend.post("/api/pcap/file", { - 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({ - processStatusCode: res.status, - processResponse: JSON.stringify(res.json) - }) - ); - }; - - resetUpload = () => { - this.setState({ - isUploadFileValid: true, - isUploadFileFocused: false, - uploadFlushAll: false, - uploadSelectedFile: null - }); - }; - - resetProcess = () => { - this.setState({ - isFileValid: true, - isFileFocused: false, - fileValue: "", - processFlushAll: false, - deleteOriginalFile: false, - }); - }; - - render() { - let sessions = this.state.sessions.map(s => - - {s["id"].substring(0, 8)} - {dateTimeToTime(s["started_at"])} - {durationBetween(s["started_at"], s["completed_at"])} - {formatSize(s["size"])} - {s["processed_packets"]} - {s["invalid_packets"]} - - download - - - ); - - const handleUploadFileChange = (file) => { - this.setState({ - isUploadFileValid: file == null || (file.type.endsWith("pcap") || file.type.endsWith("pcapng")), - isUploadFileFocused: false, - uploadSelectedFile: file, - uploadStatusCode: null, - uploadResponse: null - }); - }; - - const handleFileChange = (file) => { - this.setState({ - isFileValid: (file.endsWith("pcap") || file.endsWith("pcapng")), - isFileFocused: false, - fileValue: file, - processStatusCode: null, - processResponse: null - }); - }; - - 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 - }); - - const fileCurlCommand = createCurlCommand("pcap/file", "POST", { - file: this.state.fileValue, - flush_all: this.state.processFlushAll, - delete_original_file: this.state.deleteOriginalFile - }); - - return ( -
-
-
- GET /api/pcap/sessions - -
- -
-
- - - - - - - - - - - - - - - {sessions} - -
idstarted_atdurationsizeprocessed_packetsinvalid_packetspackets_per_serviceactions
-
-
-
- -
-
-
- POST /api/pcap/upload - -
- -
- -
-
- options: - this.setState({uploadFlushAll: v})}/> -
- -
- - -
-
- -
-
- POST /api/pcap/file - -
- -
- - -
-
- this.setState({processFlushAll: v})}/> - this.setState({deleteOriginalFile: v})}/> -
- -
- - -
-
-
-
- ); - } -} - -export default PcapPane; diff --git a/frontend/src/components/panels/PcapPane.scss b/frontend/src/components/panels/PcapPane.scss deleted file mode 100644 index 4dbc2b2..0000000 --- a/frontend/src/components/panels/PcapPane.scss +++ /dev/null @@ -1,38 +0,0 @@ -@import "../../colors.scss"; - -.pcap-pane { - display: flex; - flex-direction: column; - - .pcap-list { - overflow: hidden; - flex: 1; - - .section-content { - height: 100%; - } - - .section-table { - height: calc(100% - 30px); - } - - .table-cell-action { - font-size: 13px; - font-weight: 600; - } - } - - .upload-actions { - display: flex; - align-items: flex-end; - margin-bottom: 20px; - } - - .upload-options { - flex: 1; - - span { - font-size: 0.9em; - } - } -} diff --git a/frontend/src/components/panels/PcapsPane.js b/frontend/src/components/panels/PcapsPane.js new file mode 100644 index 0000000..8722230 --- /dev/null +++ b/frontend/src/components/panels/PcapsPane.js @@ -0,0 +1,273 @@ +/* + * This file is part of caronte (https://github.com/eciavatta/caronte). + * Copyright (c) 2020 Emiliano Ciavatta. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import React, {Component} from 'react'; +import './PcapsPane.scss'; +import './common.scss'; +import Table from "react-bootstrap/Table"; +import backend from "../../backend"; +import {createCurlCommand, dateTimeToTime, durationBetween, formatSize} from "../../utils"; +import InputField from "../fields/InputField"; +import CheckField from "../fields/CheckField"; +import TextField from "../fields/TextField"; +import ButtonField from "../fields/ButtonField"; +import LinkPopover from "../objects/LinkPopover"; +import dispatcher from "../../dispatcher"; + +class PcapsPane extends Component { + + state = { + sessions: [], + isUploadFileValid: true, + isUploadFileFocused: false, + uploadFlushAll: false, + isFileValid: true, + isFileFocused: false, + fileValue: "", + processFlushAll: false, + deleteOriginalFile: false + }; + + componentDidMount() { + this.loadSessions(); + + dispatcher.register("notifications", payload => { + if (payload.event === "pcap.upload" || payload.event === "pcap.file") { + this.loadSessions(); + } + }); + + document.title = "caronte:~/pcaps$"; + } + + loadSessions = () => { + backend.get("/api/pcap/sessions") + .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) + })); + }; + + uploadPcap = () => { + if (this.state.uploadSelectedFile == null || !this.state.isUploadFileValid) { + this.setState({isUploadFileFocused: true}); + return; + } + + 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 => { + this.setState({ + uploadStatusCode: res.status, + uploadResponse: JSON.stringify(res.json) + }); + this.resetUpload(); + this.loadSessions(); + }).catch(res => this.setState({ + uploadStatusCode: res.status, + uploadResponse: JSON.stringify(res.json) + }) + ); + }; + + processPcap = () => { + if (this.state.fileValue === "" || !this.state.isFileValid) { + this.setState({isFileFocused: true}); + return; + } + + backend.post("/api/pcap/file", { + 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({ + processStatusCode: res.status, + processResponse: JSON.stringify(res.json) + }) + ); + }; + + resetUpload = () => { + this.setState({ + isUploadFileValid: true, + isUploadFileFocused: false, + uploadFlushAll: false, + uploadSelectedFile: null + }); + }; + + resetProcess = () => { + this.setState({ + isFileValid: true, + isFileFocused: false, + fileValue: "", + processFlushAll: false, + deleteOriginalFile: false, + }); + }; + + render() { + let sessions = this.state.sessions.map(s => + + {s["id"].substring(0, 8)} + {dateTimeToTime(s["started_at"])} + {durationBetween(s["started_at"], s["completed_at"])} + {formatSize(s["size"])} + {s["processed_packets"]} + {s["invalid_packets"]} + + download + + + ); + + const handleUploadFileChange = (file) => { + this.setState({ + isUploadFileValid: file == null || (file.type.endsWith("pcap") || file.type.endsWith("pcapng")), + isUploadFileFocused: false, + uploadSelectedFile: file, + uploadStatusCode: null, + uploadResponse: null + }); + }; + + const handleFileChange = (file) => { + this.setState({ + isFileValid: (file.endsWith("pcap") || file.endsWith("pcapng")), + isFileFocused: false, + fileValue: file, + processStatusCode: null, + processResponse: null + }); + }; + + 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 + }); + + const fileCurlCommand = createCurlCommand("pcap/file", "POST", { + file: this.state.fileValue, + flush_all: this.state.processFlushAll, + delete_original_file: this.state.deleteOriginalFile + }); + + return ( +
+
+
+ GET /api/pcap/sessions + +
+ +
+
+ + + + + + + + + + + + + + + {sessions} + +
idstarted_atdurationsizeprocessed_packetsinvalid_packetspackets_per_serviceactions
+
+
+
+ +
+
+
+ POST /api/pcap/upload + +
+ +
+ +
+
+ options: + this.setState({uploadFlushAll: v})}/> +
+ +
+ + +
+
+ +
+
+ POST /api/pcap/file + +
+ +
+ + +
+
+ this.setState({processFlushAll: v})}/> + this.setState({deleteOriginalFile: v})}/> +
+ +
+ + +
+
+
+
+ ); + } +} + +export default PcapsPane; diff --git a/frontend/src/components/panels/PcapsPane.scss b/frontend/src/components/panels/PcapsPane.scss new file mode 100644 index 0000000..4dbc2b2 --- /dev/null +++ b/frontend/src/components/panels/PcapsPane.scss @@ -0,0 +1,38 @@ +@import "../../colors.scss"; + +.pcap-pane { + display: flex; + flex-direction: column; + + .pcap-list { + overflow: hidden; + flex: 1; + + .section-content { + height: 100%; + } + + .section-table { + height: calc(100% - 30px); + } + + .table-cell-action { + font-size: 13px; + font-weight: 600; + } + } + + .upload-actions { + display: flex; + align-items: flex-end; + margin-bottom: 20px; + } + + .upload-options { + flex: 1; + + span { + font-size: 0.9em; + } + } +} diff --git a/frontend/src/components/panels/RulePane.js b/frontend/src/components/panels/RulePane.js deleted file mode 100644 index 9913962..0000000 --- a/frontend/src/components/panels/RulePane.js +++ /dev/null @@ -1,438 +0,0 @@ -/* - * This file is part of caronte (https://github.com/eciavatta/caronte). - * Copyright (c) 2020 Emiliano Ciavatta. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -import React, {Component} from 'react'; -import './common.scss'; -import './RulePane.scss'; -import Table from "react-bootstrap/Table"; -import {Col, Container, Row} from "react-bootstrap"; -import InputField from "../fields/InputField"; -import CheckField from "../fields/CheckField"; -import TextField from "../fields/TextField"; -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 validation from "../../validation"; -import LinkPopover from "../objects/LinkPopover"; -import {randomClassName} from "../../utils"; -import dispatcher from "../../dispatcher"; - -const classNames = require('classnames'); -const _ = require('lodash'); - -class RulePane extends Component { - - emptyRule = { - "name": "", - "color": "", - "notes": "", - "enabled": true, - "patterns": [], - "filter": { - "service_port": 0, - "client_address": "", - "client_port": 0, - "min_duration": 0, - "max_duration": 0, - "min_bytes": 0, - "max_bytes": 0 - }, - "version": 0 - }; - emptyPattern = { - "regex": "", - "flags": { - "caseless": false, - "dot_all": false, - "multi_line": false, - "utf_8_mode": false, - "unicode_property": false - }, - "min_occurrences": 0, - "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", payload => { - if (payload.event === "rules.new" || payload.event === "rules.edit") { - this.loadRules(); - } - }); - - document.title = "caronte:~/rules$"; - } - - 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)})); - }; - - addRule = () => { - if (this.validateRule(this.state.newRule)) { - backend.post("/api/rules", this.state.newRule).then(res => { - this.reset(); - this.setState({ruleStatusCode: res.status}); - this.loadRules(); - }).catch(res => { - this.setState({ruleStatusCode: res.status, ruleResponse: JSON.stringify(res.json)}); - }); - } - }; - - updateRule = () => { - const rule = this.state.selectedRule; - if (this.validateRule(rule)) { - backend.put(`/api/rules/${rule.id}`, rule).then(res => { - this.reset(); - this.setState({ruleStatusCode: res.status}); - this.loadRules(); - }).catch(res => { - this.setState({ruleStatusCode: res.status, ruleResponse: JSON.stringify(res.json)}); - }); - } - }; - - validateRule = (rule) => { - let valid = true; - if (rule.name.length < 3) { - this.setState({ruleNameError: "name.length < 3"}); - valid = false; - } - if (!validation.isValidColor(rule.color)) { - this.setState({ruleColorError: "color is not hexcolor"}); - valid = false; - } - if (!validation.isValidPort(rule.filter.service_port)) { - this.setState({ruleServicePortError: "service_port > 65565"}); - valid = false; - } - if (!validation.isValidPort(rule.filter.client_port)) { - this.setState({ruleClientPortError: "client_port > 65565"}); - valid = false; - } - 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) { - this.setState({ruleDurationError: "min_duration > max_dur."}); - valid = false; - } - if (rule.filter.min_bytes > rule.filter.max_bytes) { - this.setState({ruleBytesError: "min_bytes > max_bytes"}); - valid = false; - } - if (rule.patterns.length < 1) { - this.setState({rulePatternsError: "patterns.length < 1"}); - valid = false; - } - - return valid; - }; - - reset = () => { - const newRule = _.cloneDeep(this.emptyRule); - const newPattern = _.cloneDeep(this.emptyPattern); - this.setState({ - selectedRule: null, - newRule: newRule, - selectedPattern: null, - newPattern: newPattern, - patternRegexFocused: false, - patternOccurrencesFocused: false, - ruleNameError: null, - ruleColorError: null, - ruleServicePortError: null, - ruleClientPortError: null, - ruleClientAddressError: null, - ruleDurationError: null, - ruleBytesError: null, - rulePatternsError: null, - ruleStatusCode: null, - rulesStatusCode: null, - ruleResponse: null, - rulesResponse: null - }); - }; - - updateParam = (callback) => { - const updatedRule = this.currentRule(); - callback(updatedRule); - this.setState({newRule: updatedRule}); - }; - - currentRule = () => this.state.selectedRule != null ? this.state.selectedRule : this.state.newRule; - - addPattern = (pattern) => { - if (!this.validatePattern(pattern)) { - return; - } - - const newPattern = _.cloneDeep(this.emptyPattern); - this.currentRule().patterns.push(pattern); - this.setState({ - newPattern: newPattern - }); - }; - - editPattern = (pattern) => { - this.setState({ - selectedPattern: pattern - }); - }; - - updatePattern = (pattern) => { - if (!this.validatePattern(pattern)) { - return; - } - - this.setState({ - selectedPattern: null - }); - }; - - validatePattern = (pattern) => { - let valid = true; - if (pattern.regex === "") { - valid = false; - this.setState({patternRegexFocused: true}); - } - if (pattern.min_occurrences > pattern.max_occurrences) { - valid = false; - this.setState({patternOccurrencesFocused: true}); - } - return valid; - }; - - render() { - const isUpdate = this.state.selectedRule != null; - const rule = this.currentRule(); - const pattern = this.state.selectedPattern || this.state.newPattern; - - let rules = this.state.rules.map(r => - { - this.reset(); - this.setState({selectedRule: _.cloneDeep(r)}); - }} className={classNames("row-small", "row-clickable", {"row-selected": rule.id === r.id})}> - {r["id"].substring(0, 8)} - {r["name"]} - - {r["notes"]} - - ); - - let patterns = (this.state.selectedPattern == null && !isUpdate ? - rule.patterns.concat(this.state.newPattern) : - rule.patterns - ).map(p => p === pattern ? - - - { - this.updateParam(() => pattern.regex = v); - this.setState({patternRegexFocused: pattern.regex === ""}); - }}/> - - this.updateParam(() => pattern.flags.caseless = v)}/> - this.updateParam(() => pattern.flags.dot_all = v)}/> - this.updateParam(() => pattern.flags.multi_line = v)}/> - this.updateParam(() => pattern.flags.utf_8_mode = v)}/> - this.updateParam(() => pattern.flags.unicode_property = v)}/> - - this.updateParam(() => pattern.min_occurrences = v)}/> - - - this.updateParam(() => pattern.max_occurrences = v)}/> - - s", "s->c"]} - value={this.directions[pattern.direction]} - onChange={(v) => this.updateParam(() => pattern.direction = v)}/> - {this.state.selectedPattern == null ? - this.addPattern(p)}/> : - this.updatePattern(p)}/>} - - - : - - {p.regex} - {p.flags.caseless ? "yes" : "no"} - {p.flags.dot_all ? "yes" : "no"} - {p.flags.multi_line ? "yes" : "no"} - {p.flags.utf_8_mode ? "yes" : "no"} - {p.flags.unicode_property ? "yes" : "no"} - {p.min_occurrences} - {p.max_occurrences} - {this.directions[p.direction]} - {!isUpdate && this.editPattern(p)}/>} - - ); - - return ( -
-
-
- GET /api/rules - {this.state.rulesStatusCode && - } -
- -
-
- - - - - - - - - - - {rules} - -
idnamecolornotes
-
-
-
- -
-
- - {isUpdate ? `PUT /api/rules/${this.state.selectedRule.id}` : "POST /api/rules"} - - -
- -
- - - - this.updateParam((r) => r.name = v)} - error={this.state.ruleNameError}/> - this.updateParam((r) => r.color = v)}/> - this.updateParam((r) => r.notes = v)}/> - - - - filters: - this.updateParam((r) => r.filter.service_port = v)} - min={0} max={65565} error={this.state.ruleServicePortError} - readonly={isUpdate}/> - this.updateParam((r) => r.filter.client_port = v)} - min={0} max={65565} error={this.state.ruleClientPortError} - readonly={isUpdate}/> - this.updateParam((r) => r.filter.client_address = v)}/> - - - - this.updateParam((r) => r.filter.min_duration = v)}/> - this.updateParam((r) => r.filter.max_duration = v)}/> - this.updateParam((r) => r.filter.min_bytes = v)}/> - this.updateParam((r) => r.filter.max_bytes = v)}/> - - - - -
- - - - - - - - - - - - - {!isUpdate && } - - - - {patterns} - -
regex!Aa.*\n+UTF8Uni_minmaxdirectionactions
- {this.state.rulePatternsError != null && - error: {this.state.rulePatternsError}} -
-
- -
- {} - -
-
-
- ); - } - -} - -export default RulePane; diff --git a/frontend/src/components/panels/RulePane.scss b/frontend/src/components/panels/RulePane.scss deleted file mode 100644 index 992445a..0000000 --- a/frontend/src/components/panels/RulePane.scss +++ /dev/null @@ -1,32 +0,0 @@ - -.rule-pane { - display: flex; - flex-direction: column; - - .rules-list { - overflow: hidden; - flex: 2 1; - - .section-content { - height: 100%; - } - - .section-table { - height: calc(100% - 30px); - } - } - - .rule-edit { - display: flex; - flex: 3 0; - flex-direction: column; - - .section-content { - flex: 1; - } - - .section-table { - max-height: 150px; - } - } -} diff --git a/frontend/src/components/panels/RulesPane.js b/frontend/src/components/panels/RulesPane.js new file mode 100644 index 0000000..a66cde7 --- /dev/null +++ b/frontend/src/components/panels/RulesPane.js @@ -0,0 +1,438 @@ +/* + * This file is part of caronte (https://github.com/eciavatta/caronte). + * Copyright (c) 2020 Emiliano Ciavatta. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import React, {Component} from 'react'; +import './common.scss'; +import './RulesPane.scss'; +import Table from "react-bootstrap/Table"; +import {Col, Container, Row} from "react-bootstrap"; +import InputField from "../fields/InputField"; +import CheckField from "../fields/CheckField"; +import TextField from "../fields/TextField"; +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 validation from "../../validation"; +import LinkPopover from "../objects/LinkPopover"; +import {randomClassName} from "../../utils"; +import dispatcher from "../../dispatcher"; + +const classNames = require('classnames'); +const _ = require('lodash'); + +class RulesPane extends Component { + + emptyRule = { + "name": "", + "color": "", + "notes": "", + "enabled": true, + "patterns": [], + "filter": { + "service_port": 0, + "client_address": "", + "client_port": 0, + "min_duration": 0, + "max_duration": 0, + "min_bytes": 0, + "max_bytes": 0 + }, + "version": 0 + }; + emptyPattern = { + "regex": "", + "flags": { + "caseless": false, + "dot_all": false, + "multi_line": false, + "utf_8_mode": false, + "unicode_property": false + }, + "min_occurrences": 0, + "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", payload => { + if (payload.event === "rules.new" || payload.event === "rules.edit") { + this.loadRules(); + } + }); + + document.title = "caronte:~/rules$"; + } + + 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)})); + }; + + addRule = () => { + if (this.validateRule(this.state.newRule)) { + backend.post("/api/rules", this.state.newRule).then(res => { + this.reset(); + this.setState({ruleStatusCode: res.status}); + this.loadRules(); + }).catch(res => { + this.setState({ruleStatusCode: res.status, ruleResponse: JSON.stringify(res.json)}); + }); + } + }; + + updateRule = () => { + const rule = this.state.selectedRule; + if (this.validateRule(rule)) { + backend.put(`/api/rules/${rule.id}`, rule).then(res => { + this.reset(); + this.setState({ruleStatusCode: res.status}); + this.loadRules(); + }).catch(res => { + this.setState({ruleStatusCode: res.status, ruleResponse: JSON.stringify(res.json)}); + }); + } + }; + + validateRule = (rule) => { + let valid = true; + if (rule.name.length < 3) { + this.setState({ruleNameError: "name.length < 3"}); + valid = false; + } + if (!validation.isValidColor(rule.color)) { + this.setState({ruleColorError: "color is not hexcolor"}); + valid = false; + } + if (!validation.isValidPort(rule.filter.service_port)) { + this.setState({ruleServicePortError: "service_port > 65565"}); + valid = false; + } + if (!validation.isValidPort(rule.filter.client_port)) { + this.setState({ruleClientPortError: "client_port > 65565"}); + valid = false; + } + 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) { + this.setState({ruleDurationError: "min_duration > max_dur."}); + valid = false; + } + if (rule.filter.min_bytes > rule.filter.max_bytes) { + this.setState({ruleBytesError: "min_bytes > max_bytes"}); + valid = false; + } + if (rule.patterns.length < 1) { + this.setState({rulePatternsError: "patterns.length < 1"}); + valid = false; + } + + return valid; + }; + + reset = () => { + const newRule = _.cloneDeep(this.emptyRule); + const newPattern = _.cloneDeep(this.emptyPattern); + this.setState({ + selectedRule: null, + newRule: newRule, + selectedPattern: null, + newPattern: newPattern, + patternRegexFocused: false, + patternOccurrencesFocused: false, + ruleNameError: null, + ruleColorError: null, + ruleServicePortError: null, + ruleClientPortError: null, + ruleClientAddressError: null, + ruleDurationError: null, + ruleBytesError: null, + rulePatternsError: null, + ruleStatusCode: null, + rulesStatusCode: null, + ruleResponse: null, + rulesResponse: null + }); + }; + + updateParam = (callback) => { + const updatedRule = this.currentRule(); + callback(updatedRule); + this.setState({newRule: updatedRule}); + }; + + currentRule = () => this.state.selectedRule != null ? this.state.selectedRule : this.state.newRule; + + addPattern = (pattern) => { + if (!this.validatePattern(pattern)) { + return; + } + + const newPattern = _.cloneDeep(this.emptyPattern); + this.currentRule().patterns.push(pattern); + this.setState({ + newPattern: newPattern + }); + }; + + editPattern = (pattern) => { + this.setState({ + selectedPattern: pattern + }); + }; + + updatePattern = (pattern) => { + if (!this.validatePattern(pattern)) { + return; + } + + this.setState({ + selectedPattern: null + }); + }; + + validatePattern = (pattern) => { + let valid = true; + if (pattern.regex === "") { + valid = false; + this.setState({patternRegexFocused: true}); + } + if (pattern.min_occurrences > pattern.max_occurrences) { + valid = false; + this.setState({patternOccurrencesFocused: true}); + } + return valid; + }; + + render() { + const isUpdate = this.state.selectedRule != null; + const rule = this.currentRule(); + const pattern = this.state.selectedPattern || this.state.newPattern; + + let rules = this.state.rules.map(r => + { + this.reset(); + this.setState({selectedRule: _.cloneDeep(r)}); + }} className={classNames("row-small", "row-clickable", {"row-selected": rule.id === r.id})}> + {r["id"].substring(0, 8)} + {r["name"]} + + {r["notes"]} + + ); + + let patterns = (this.state.selectedPattern == null && !isUpdate ? + rule.patterns.concat(this.state.newPattern) : + rule.patterns + ).map(p => p === pattern ? + + + { + this.updateParam(() => pattern.regex = v); + this.setState({patternRegexFocused: pattern.regex === ""}); + }}/> + + this.updateParam(() => pattern.flags.caseless = v)}/> + this.updateParam(() => pattern.flags.dot_all = v)}/> + this.updateParam(() => pattern.flags.multi_line = v)}/> + this.updateParam(() => pattern.flags.utf_8_mode = v)}/> + this.updateParam(() => pattern.flags.unicode_property = v)}/> + + this.updateParam(() => pattern.min_occurrences = v)}/> + + + this.updateParam(() => pattern.max_occurrences = v)}/> + + s", "s->c"]} + value={this.directions[pattern.direction]} + onChange={(v) => this.updateParam(() => pattern.direction = v)}/> + {this.state.selectedPattern == null ? + this.addPattern(p)}/> : + this.updatePattern(p)}/>} + + + : + + {p.regex} + {p.flags.caseless ? "yes" : "no"} + {p.flags.dot_all ? "yes" : "no"} + {p.flags.multi_line ? "yes" : "no"} + {p.flags.utf_8_mode ? "yes" : "no"} + {p.flags.unicode_property ? "yes" : "no"} + {p.min_occurrences} + {p.max_occurrences} + {this.directions[p.direction]} + {!isUpdate && this.editPattern(p)}/>} + + ); + + return ( +
+
+
+ GET /api/rules + {this.state.rulesStatusCode && + } +
+ +
+
+ + + + + + + + + + + {rules} + +
idnamecolornotes
+
+
+
+ +
+
+ + {isUpdate ? `PUT /api/rules/${this.state.selectedRule.id}` : "POST /api/rules"} + + +
+ +
+ + + + this.updateParam((r) => r.name = v)} + error={this.state.ruleNameError}/> + this.updateParam((r) => r.color = v)}/> + this.updateParam((r) => r.notes = v)}/> + + + + filters: + this.updateParam((r) => r.filter.service_port = v)} + min={0} max={65565} error={this.state.ruleServicePortError} + readonly={isUpdate}/> + this.updateParam((r) => r.filter.client_port = v)} + min={0} max={65565} error={this.state.ruleClientPortError} + readonly={isUpdate}/> + this.updateParam((r) => r.filter.client_address = v)}/> + + + + this.updateParam((r) => r.filter.min_duration = v)}/> + this.updateParam((r) => r.filter.max_duration = v)}/> + this.updateParam((r) => r.filter.min_bytes = v)}/> + this.updateParam((r) => r.filter.max_bytes = v)}/> + + + + +
+ + + + + + + + + + + + + {!isUpdate && } + + + + {patterns} + +
regex!Aa.*\n+UTF8Uni_minmaxdirectionactions
+ {this.state.rulePatternsError != null && + error: {this.state.rulePatternsError}} +
+
+ +
+ {} + +
+
+
+ ); + } + +} + +export default RulesPane; diff --git a/frontend/src/components/panels/RulesPane.scss b/frontend/src/components/panels/RulesPane.scss new file mode 100644 index 0000000..992445a --- /dev/null +++ b/frontend/src/components/panels/RulesPane.scss @@ -0,0 +1,32 @@ + +.rule-pane { + display: flex; + flex-direction: column; + + .rules-list { + overflow: hidden; + flex: 2 1; + + .section-content { + height: 100%; + } + + .section-table { + height: calc(100% - 30px); + } + } + + .rule-edit { + display: flex; + flex: 3 0; + flex-direction: column; + + .section-content { + flex: 1; + } + + .section-table { + max-height: 150px; + } + } +} diff --git a/frontend/src/components/panels/ServicePane.js b/frontend/src/components/panels/ServicePane.js deleted file mode 100644 index fc7004b..0000000 --- a/frontend/src/components/panels/ServicePane.js +++ /dev/null @@ -1,212 +0,0 @@ -/* - * This file is part of caronte (https://github.com/eciavatta/caronte). - * Copyright (c) 2020 Emiliano Ciavatta. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -import React, {Component} from 'react'; -import './common.scss'; -import './ServicePane.scss'; -import Table from "react-bootstrap/Table"; -import {Col, Container, Row} from "react-bootstrap"; -import InputField from "../fields/InputField"; -import TextField from "../fields/TextField"; -import backend from "../../backend"; -import NumericField from "../fields/extensions/NumericField"; -import ColorField from "../fields/extensions/ColorField"; -import ButtonField from "../fields/ButtonField"; -import validation from "../../validation"; -import LinkPopover from "../objects/LinkPopover"; -import {createCurlCommand} from "../../utils"; -import dispatcher from "../../dispatcher"; - -const classNames = require('classnames'); -const _ = require('lodash'); - -class ServicePane extends Component { - - emptyService = { - "port": 0, - "name": "", - "color": "", - "notes": "" - }; - - state = { - services: [], - currentService: this.emptyService, - }; - - componentDidMount() { - this.reset(); - this.loadServices(); - - dispatcher.register("notifications", payload => { - if (payload.event === "services.edit") { - this.loadServices(); - } - }); - - document.title = "caronte:~/services$"; - } - - 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)})); - }; - - updateService = () => { - const service = this.state.currentService; - if (this.validateService(service)) { - backend.put("/api/services", service).then(res => { - this.reset(); - this.setState({serviceStatusCode: res.status}); - this.loadServices(); - }).catch(res => { - this.setState({serviceStatusCode: res.status, serviceResponse: JSON.stringify(res.json)}); - }); - } - }; - - validateService = (service) => { - let valid = true; - if (!validation.isValidPort(service.port, true)) { - this.setState({servicePortError: "port < 0 || port > 65565"}); - valid = false; - } - if (service.name.length < 3) { - this.setState({serviceNameError: "name.length < 3"}); - valid = false; - } - if (!validation.isValidColor(service.color)) { - this.setState({serviceColorError: "color is not hexcolor"}); - valid = false; - } - - return valid; - }; - - reset = () => { - this.setState({ - isUpdate: false, - currentService: _.cloneDeep(this.emptyService), - servicePortError: null, - serviceNameError: null, - serviceColorError: null, - serviceStatusCode: null, - servicesStatusCode: null, - serviceResponse: null, - servicesResponse: null - }); - }; - - updateParam = (callback) => { - callback(this.state.currentService); - this.setState({currentService: this.state.currentService}); - }; - - render() { - const isUpdate = this.state.isUpdate; - const service = this.state.currentService; - - let services = this.state.services.map(s => - { - this.reset(); - this.setState({isUpdate: true, currentService: _.cloneDeep(s)}); - }} className={classNames("row-small", "row-clickable", {"row-selected": service.port === s.port })}> - {s["port"]} - {s["name"]} - - {s["notes"]} - - ); - - const curlCommand = createCurlCommand("/services", "PUT", service); - - return ( -
-
-
- GET /api/services - {this.state.servicesStatusCode && - } -
- -
-
- - - - - - - - - - - {services} - -
portnamecolornotes
-
-
-
- -
-
- PUT /api/services - -
- -
- - - - this.updateParam((s) => s.port = v)} - min={0} max={65565} error={this.state.servicePortError} /> - this.updateParam((s) => s.name = v)} - error={this.state.serviceNameError} /> - this.updateParam((s) => s.color = v)} /> - - - - this.updateParam((s) => s.notes = v)} /> - - - - - -
- -
- {} - -
-
-
- ); - } - -} - -export default ServicePane; diff --git a/frontend/src/components/panels/ServicePane.scss b/frontend/src/components/panels/ServicePane.scss deleted file mode 100644 index daf7e79..0000000 --- a/frontend/src/components/panels/ServicePane.scss +++ /dev/null @@ -1,22 +0,0 @@ - -.service-pane { - display: flex; - flex-direction: column; - - .services-list { - overflow: hidden; - flex: 1; - - .section-content { - height: 100%; - } - - .section-table { - height: calc(100% - 30px); - } - } - - .service-edit { - flex: 0; - } -} diff --git a/frontend/src/components/panels/ServicesPane.js b/frontend/src/components/panels/ServicesPane.js new file mode 100644 index 0000000..bc82356 --- /dev/null +++ b/frontend/src/components/panels/ServicesPane.js @@ -0,0 +1,212 @@ +/* + * This file is part of caronte (https://github.com/eciavatta/caronte). + * Copyright (c) 2020 Emiliano Ciavatta. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import React, {Component} from 'react'; +import './common.scss'; +import './ServicesPane.scss'; +import Table from "react-bootstrap/Table"; +import {Col, Container, Row} from "react-bootstrap"; +import InputField from "../fields/InputField"; +import TextField from "../fields/TextField"; +import backend from "../../backend"; +import NumericField from "../fields/extensions/NumericField"; +import ColorField from "../fields/extensions/ColorField"; +import ButtonField from "../fields/ButtonField"; +import validation from "../../validation"; +import LinkPopover from "../objects/LinkPopover"; +import {createCurlCommand} from "../../utils"; +import dispatcher from "../../dispatcher"; + +const classNames = require('classnames'); +const _ = require('lodash'); + +class ServicesPane extends Component { + + emptyService = { + "port": 0, + "name": "", + "color": "", + "notes": "" + }; + + state = { + services: [], + currentService: this.emptyService, + }; + + componentDidMount() { + this.reset(); + this.loadServices(); + + dispatcher.register("notifications", payload => { + if (payload.event === "services.edit") { + this.loadServices(); + } + }); + + document.title = "caronte:~/services$"; + } + + 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)})); + }; + + updateService = () => { + const service = this.state.currentService; + if (this.validateService(service)) { + backend.put("/api/services", service).then(res => { + this.reset(); + this.setState({serviceStatusCode: res.status}); + this.loadServices(); + }).catch(res => { + this.setState({serviceStatusCode: res.status, serviceResponse: JSON.stringify(res.json)}); + }); + } + }; + + validateService = (service) => { + let valid = true; + if (!validation.isValidPort(service.port, true)) { + this.setState({servicePortError: "port < 0 || port > 65565"}); + valid = false; + } + if (service.name.length < 3) { + this.setState({serviceNameError: "name.length < 3"}); + valid = false; + } + if (!validation.isValidColor(service.color)) { + this.setState({serviceColorError: "color is not hexcolor"}); + valid = false; + } + + return valid; + }; + + reset = () => { + this.setState({ + isUpdate: false, + currentService: _.cloneDeep(this.emptyService), + servicePortError: null, + serviceNameError: null, + serviceColorError: null, + serviceStatusCode: null, + servicesStatusCode: null, + serviceResponse: null, + servicesResponse: null + }); + }; + + updateParam = (callback) => { + callback(this.state.currentService); + this.setState({currentService: this.state.currentService}); + }; + + render() { + const isUpdate = this.state.isUpdate; + const service = this.state.currentService; + + let services = this.state.services.map(s => + { + this.reset(); + this.setState({isUpdate: true, currentService: _.cloneDeep(s)}); + }} className={classNames("row-small", "row-clickable", {"row-selected": service.port === s.port })}> + {s["port"]} + {s["name"]} + + {s["notes"]} + + ); + + const curlCommand = createCurlCommand("/services", "PUT", service); + + return ( +
+
+
+ GET /api/services + {this.state.servicesStatusCode && + } +
+ +
+
+ + + + + + + + + + + {services} + +
portnamecolornotes
+
+
+
+ +
+
+ PUT /api/services + +
+ +
+ + + + this.updateParam((s) => s.port = v)} + min={0} max={65565} error={this.state.servicePortError} /> + this.updateParam((s) => s.name = v)} + error={this.state.serviceNameError} /> + this.updateParam((s) => s.color = v)} /> + + + + this.updateParam((s) => s.notes = v)} /> + + + + + +
+ +
+ {} + +
+
+
+ ); + } + +} + +export default ServicesPane; diff --git a/frontend/src/components/panels/ServicesPane.scss b/frontend/src/components/panels/ServicesPane.scss new file mode 100644 index 0000000..daf7e79 --- /dev/null +++ b/frontend/src/components/panels/ServicesPane.scss @@ -0,0 +1,22 @@ + +.service-pane { + display: flex; + flex-direction: column; + + .services-list { + overflow: hidden; + flex: 1; + + .section-content { + height: 100%; + } + + .section-table { + height: calc(100% - 30px); + } + } + + .service-edit { + flex: 0; + } +} diff --git a/frontend/src/components/panels/StreamsPane.js b/frontend/src/components/panels/StreamsPane.js new file mode 100644 index 0000000..c8bd121 --- /dev/null +++ b/frontend/src/components/panels/StreamsPane.js @@ -0,0 +1,242 @@ +/* + * This file is part of caronte (https://github.com/eciavatta/caronte). + * Copyright (c) 2020 Emiliano Ciavatta. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import React, {Component} from 'react'; +import './StreamsPane.scss'; +import {Row} from 'react-bootstrap'; +import MessageAction from "../objects/MessageAction"; +import backend from "../../backend"; +import ButtonField from "../fields/ButtonField"; +import ChoiceField from "../fields/ChoiceField"; +import DOMPurify from 'dompurify'; +import ReactJson from 'react-json-view' +import {downloadBlob, getHeaderValue} from "../../utils"; +import log from "../../log"; + +const classNames = require('classnames'); + +class StreamsPane extends Component { + + state = { + messages: [], + format: "default", + tryParse: true + }; + + constructor(props) { + super(props); + + this.validFormats = ["default", "hex", "hexdump", "base32", "base64", "ascii", "binary", "decimal", "octal"]; + } + + componentDidMount() { + if (this.props.connection && this.state.currentId !== this.props.connection.id) { + this.setState({currentId: this.props.connection.id}); + this.loadStream(this.props.connection.id); + } + + document.title = "caronte:~/$"; + } + + componentDidUpdate(prevProps, prevState, snapshot) { + if (this.props.connection && ( + this.props.connection !== prevProps.connection || this.state.format !== prevState.format)) { + this.closeRenderWindow(); + this.loadStream(this.props.connection.id); + } + } + + componentWillUnmount() { + this.closeRenderWindow(); + } + + loadStream = (connectionId) => { + this.setState({messages: []}); + backend.get(`/api/streams/${connectionId}?format=${this.state.format}`) + .then(res => this.setState({messages: res.json})); + }; + + setFormat = (format) => { + if (this.validFormats.includes(format)) { + this.setState({format: format}); + } + }; + + tryParseConnectionMessage = (connectionMessage) => { + if (connectionMessage.metadata == null) { + return connectionMessage.content; + } + if (connectionMessage["is_metadata_continuation"]) { + return **already parsed in previous messages**; + } + + let unrollMap = (obj) => obj == null ? null : Object.entries(obj).map(([key, value]) => +

{key}: {value}

+ ); + + let m = connectionMessage.metadata; + switch (m.type) { + case "http-request": + let url = {m.host}{m.url}; + return +

{m.method} {url} {m.protocol}

+ {unrollMap(m.headers)} +
{m.body}
+ {unrollMap(m.trailers)} +
; + case "http-response": + const contentType = getHeaderValue(m, "Content-Type"); + let body = m.body; + if (contentType && contentType.includes("application/json")) { + try { + const json = JSON.parse(m.body); + body = ; + } catch (e) { + console.log(e); + } + } + + return +

{m.protocol} {m.status}

+ {unrollMap(m.headers)} +
{body}
+ {unrollMap(m.trailers)} +
; + default: + return connectionMessage.content; + } + }; + + connectionsActions = (connectionMessage) => { + if (!connectionMessage.metadata) { + return null; + } + + const m = connectionMessage.metadata; + switch (m.type) { + case "http-request" : + if (!connectionMessage.metadata["reproducers"]) { + return; + } + return Object.entries(connectionMessage.metadata["reproducers"]).map(([actionName, actionValue]) => + { + this.setState({ + messageActionDialog: this.setState({messageActionDialog: null})}/> + }); + }}/> + ); + case "http-response": + const contentType = getHeaderValue(m, "Content-Type"); + + if (contentType && contentType.includes("text/html")) { + return { + let w; + if (this.state.renderWindow && !this.state.renderWindow.closed) { + w = this.state.renderWindow; + } else { + w = window.open("", "", "width=900, height=600, scrollbars=yes"); + this.setState({renderWindow: w}); + } + w.document.body.innerHTML = DOMPurify.sanitize(m.body); + w.focus(); + }}/>; + } + break; + default: + return null; + } + }; + + downloadStreamRaw = (value) => { + if (this.state.currentId) { + backend.download(`/api/streams/${this.props.connection.id}/download?format=${this.state.format}&type=${value}`) + .then(res => downloadBlob(res.blob, `${this.state.currentId}-${value}-${this.state.format}.txt`)) + .catch(_ => log.error("Failed to download stream messages")); + } + }; + + closeRenderWindow = () => { + if (this.state.renderWindow) { + this.state.renderWindow.close(); + } + }; + + render() { + const conn = this.props.connection || { + "ip_src": "0.0.0.0", + "ip_dst": "0.0.0.0", + "port_src": "0", + "port_dst": "0", + "started_at": new Date().toISOString(), + }; + const content = this.state.messages || []; + + let payload = content.map((c, i) => +
+
+
+
+ offset: {c.index} | timestamp: {c.timestamp} + | retransmitted: {c["is_retransmitted"] ? "yes" : "no"} +
+
{this.connectionsActions(c)}
+
+
+
{c["from_client"] ? "client" : "server"}
+
+ {this.state.tryParse && this.state.format === "default" ? this.tryParseConnectionMessage(c) : c.content} +
+
+ ); + + return ( +
+
+ +
+ flow: {conn["ip_src"]}:{conn["port_src"]} -> {conn["ip_dst"]}:{conn["port_dst"]} + | timestamp: {conn["started_at"]} +
+
+ + + + + +
+
+
+ +
{payload}
+ {this.state.messageActionDialog} +
+ ); + } + +} + + +export default StreamsPane; diff --git a/frontend/src/components/panels/StreamsPane.scss b/frontend/src/components/panels/StreamsPane.scss new file mode 100644 index 0000000..d5510cf --- /dev/null +++ b/frontend/src/components/panels/StreamsPane.scss @@ -0,0 +1,113 @@ +@import "../../colors"; + +.connection-content { + height: 100%; + background-color: $color-primary-0; + + pre { + overflow-x: hidden; + height: calc(100% - 31px); + padding: 0 10px; + white-space: pre-wrap; + word-break: break-word; + + p { + margin: 0; + padding: 0; + } + } + + .connection-message { + position: relative; + margin: 10px 0; + border: 4px solid $color-primary-3; + border-top: 0; + + .connection-message-header { + height: 25px; + background-color: $color-primary-3; + + .connection-message-info { + font-size: 11px; + margin-top: 6px; + margin-left: -10px; + } + + .connection-message-actions { + display: none; + margin-right: -18px; + + button { + font-size: 11px; + margin: 0 3px; + padding: 5px; + } + } + } + + .message-content { + padding: 10px; + + .react-json-view { + background-color: inherit !important; + } + } + + &:hover .connection-message-actions { + display: flex; + } + + .connection-message-label { + font-size: 12px; + position: absolute; + top: 0; + padding: 10px 0; + background-color: $color-primary-3; + writing-mode: vertical-rl; + text-orientation: mixed; + } + + &.from-client { + margin-right: 100px; + color: $color-primary-4; + + .connection-message-label { + right: -22px; + } + } + + &.from-server { + margin-left: 100px; + color: $color-primary-4; + + .connection-message-label { + left: -22px; + transform: rotate(-180deg); + } + } + } + + .connection-content-header { + height: 33px; + padding: 0; + background-color: $color-primary-3; + + .header-info { + font-size: 12px; + padding-top: 7px; + padding-left: 25px; + } + + .header-actions { + display: flex; + + .choice-field { + margin-top: -5px; + + .field-value { + background-color: $color-primary-3; + } + } + } + } +} diff --git a/frontend/src/index.js b/frontend/src/index.js index e3e48de..ca1273a 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -19,7 +19,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import 'bootstrap/dist/css/bootstrap.css'; import './index.scss'; -import App from './views/App'; +import App from './components/App'; import * as serviceWorker from './serviceWorker'; import notifications from "./notifications"; diff --git a/frontend/src/views/App.js b/frontend/src/views/App.js deleted file mode 100644 index 8105117..0000000 --- a/frontend/src/views/App.js +++ /dev/null @@ -1,81 +0,0 @@ -/* - * This file is part of caronte (https://github.com/eciavatta/caronte). - * Copyright (c) 2020 Emiliano Ciavatta. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -import React, {Component} from 'react'; -import './App.scss'; -import Header from "./Header"; -import MainPane from "../components/panels/MainPane"; -import Timeline from "./Timeline"; -import {BrowserRouter as Router} from "react-router-dom"; -import Filters from "./Filters"; -import ConfigurationPane from "../components/panels/ConfigurationPane"; -import Notifications from "../components/Notifications"; -import dispatcher from "../dispatcher"; - -class App extends Component { - - state = {}; - - componentDidMount() { - dispatcher.register("notifications", payload => { - if (payload.event === "connected") { - this.setState({ - connected: true, - configured: payload.message["is_configured"] - }); - } - }); - - setInterval(() => { - if (document.title.endsWith("❚")) { - document.title = document.title.slice(0, -1); - } else { - document.title += "❚"; - } - }, 500); - } - - render() { - let modal; - if (this.state.filterWindowOpen && this.state.configured) { - modal = this.setState({filterWindowOpen: false})}/>; - } - - return ( -
- - {this.state.connected && - -
-
this.setState({filterWindowOpen: true})}/> -
-
- {this.state.configured ? : - this.setState({configured: true})}/>} - {modal} -
-
- {this.state.configured && } -
-
- } -
- ); - } -} - -export default App; diff --git a/frontend/src/views/App.scss b/frontend/src/views/App.scss deleted file mode 100644 index 87661c3..0000000 --- a/frontend/src/views/App.scss +++ /dev/null @@ -1,16 +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 b2edd3f..0000000 --- a/frontend/src/views/Connections.js +++ /dev/null @@ -1,293 +0,0 @@ -/* - * This file is part of caronte (https://github.com/eciavatta/caronte). - * Copyright (c) 2020 Emiliano Ciavatta. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -import React, {Component} from 'react'; -import './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"; -import log from "../log"; -import ButtonField from "../components/fields/ButtonField"; -import dispatcher from "../dispatcher"; - -class Connections 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; - this.doQueryStringRedirect = false; - this.doSelectedConnectionRedirect = false; - } - - componentDidMount() { - this.loadConnections({limit: this.queryLimit}) - .then(() => this.setState({loaded: true})); - if (this.props.initialConnection) { - this.setState({selected: this.props.initialConnection.id}); - } - - dispatcher.register("timeline_updates", 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}`)); - }); - - dispatcher.register("notifications", payload => { - if (payload.event === "rules.new" || payload.event === "rules.edit") { - this.loadRules().then(() => log.debug("Loaded connection rules after notification update")); - } - }); - - dispatcher.register("notifications", payload => { - if (payload.event === "services.edit") { - this.loadServices().then(() => log.debug("Services reloaded after notification update")); - } - }); - } - - connectionSelected = (c) => { - this.doSelectedConnectionRedirect = true; - 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.loadConnections({limit: this.queryLimit}) - .then(() => log.info("Connections reloaded after query string update")); - } - } - - 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; - }; - - addServicePortFilter = (port) => { - const urlParams = new URLSearchParams(this.props.location.search); - urlParams.set("service_port", port); - this.doQueryStringRedirect = true; - 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.doQueryStringRedirect = true; - 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}); - if (!this.state.rules) { - await this.loadRules(); - } - if (!this.state.services) { - await this.loadServices(); - } - - 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 && params.to === undefined) { - if (res.length > 0) { - connections = this.state.connections.concat(res.slice(1)); - 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 && params.from === undefined) { - 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: connections, - firstConnection: firstConnection, - lastConnection: 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.doSelectedConnectionRedirect) { - redirect = ; - this.doSelectedConnectionRedirect = false; - } else if (this.doQueryStringRedirect) { - redirect = ; - this.doQueryStringRedirect = false; - } - - let loading = null; - if (this.state.loading) { - loading = - Loading... - ; - } - - return ( -
- {this.state.showMoreRecentButton &&
- { - this.disableScrollHandler = true; - this.connectionsListRef.current.scrollTop = 0; - this.loadConnections({limit: this.queryLimit}) - .then(() => { - this.disableScrollHandler = false; - log.info("Most recent connections loaded"); - }); - }}/> -
} - -
- - - - - - - - - - - - - - - - - { - this.state.connections.flatMap(c => { - return [ this.connectionSelected(c)} - selected={this.state.selected === c.id} - onMarked={marked => c.marked = marked} - onEnabled={enabled => c.hidden = !enabled} - addServicePortFilter={this.addServicePortFilter} - services={this.state.services}/>, - c.matched_rules.length > 0 && - - ]; - }) - } - {loading} - -
servicesrcipsrcportdstipdstportstarted_atdurationupdownactions
- - {redirect} -
-
- ); - } - -} - -export default withRouter(Connections); diff --git a/frontend/src/views/Connections.scss b/frontend/src/views/Connections.scss deleted file mode 100644 index de06096..0000000 --- a/frontend/src/views/Connections.scss +++ /dev/null @@ -1,38 +0,0 @@ -@import "../colors.scss"; - -.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); - background-color: red; - } -} diff --git a/frontend/src/views/Filters.js b/frontend/src/views/Filters.js deleted file mode 100644 index 3dd8280..0000000 --- a/frontend/src/views/Filters.js +++ /dev/null @@ -1,116 +0,0 @@ -/* - * This file is part of caronte (https://github.com/eciavatta/caronte). - * Copyright (c) 2020 Emiliano Ciavatta. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -import React, {Component} from 'react'; -import {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 => - - this.checkboxChangesHandler(name, event)}/> - {filtersDefinitions[name]} - - ); - } - - render() { - return ( - - - - ~/filters - - - - - - - - - - - - - - - {this.generateRows(["service_port", "client_address", "min_duration", - "min_bytes", "started_after", "closed_after", "marked"])} - -
showfilter
- - - - - - - - - - - {this.generateRows(["matched_rules", "client_port", "max_duration", - "max_bytes", "started_before", "closed_before", "hidden"])} - -
showfilter
- - -
- - -
-
- - - -
- ); - } -} - -export default Filters; diff --git a/frontend/src/views/Header.js b/frontend/src/views/Header.js deleted file mode 100644 index 2cfe9fb..0000000 --- a/frontend/src/views/Header.js +++ /dev/null @@ -1,109 +0,0 @@ -/* - * This file is part of caronte (https://github.com/eciavatta/caronte). - * Copyright (c) 2020 Emiliano Ciavatta. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -import React, {Component} from 'react'; -import Typed from 'typed.js'; -import './Header.scss'; -import {filtersDefinitions, filtersNames} from "../components/filters/FiltersDefinitions"; -import {Link, withRouter} 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 => {filtersDefinitions[name]}) - .slice(0, 5); - - return ( -
-
-
-

- { - this.el = el; - }}/> -

-
- -
-
- {quickFilters} -
-
- -
-
- - - - - - - - - - - - - -
-
-
-
- ); - } -} - -export default withRouter(Header); diff --git a/frontend/src/views/Header.scss b/frontend/src/views/Header.scss deleted file mode 100644 index 0711159..0000000 --- a/frontend/src/views/Header.scss +++ /dev/null @@ -1,34 +0,0 @@ -@import "../colors.scss"; - -.header { - height: 80px; - padding: 15px 30px; - - > .row { - background-color: $color-primary-0; - } - - .header-title { - width: 200px; - margin: 5px 0 5px -5px; - } - - .header-buttons { - display: flex; - justify-content: flex-end; - margin: 7px 0; - - .button-field { - margin-left: 7px; - } - } - - .filters-bar { - padding: 3px 0; - - .filter { - display: inline-block; - margin-right: 10px; - } - } -} diff --git a/frontend/src/views/Timeline.js b/frontend/src/views/Timeline.js deleted file mode 100644 index ebe3eb9..0000000 --- a/frontend/src/views/Timeline.js +++ /dev/null @@ -1,232 +0,0 @@ -/* - * This file is part of caronte (https://github.com/eciavatta/caronte). - * Copyright (c) 2020 Emiliano Ciavatta. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -import React, {Component} from 'react'; -import './Timeline.scss'; -import { - ChartContainer, - ChartRow, - Charts, - LineChart, - MultiBrush, - Resizable, - styler, - YAxis -} from "react-timeseries-charts"; -import {TimeRange, TimeSeries} from "pondjs"; -import backend from "../backend"; -import ChoiceField from "../components/fields/ChoiceField"; -import {withRouter} from "react-router-dom"; -import log from "../log"; -import dispatcher from "../dispatcher"; - -const minutes = 60 * 1000; - -class Timeline extends Component { - - state = { - metric: "connections_per_service" - }; - - constructor() { - super(); - - this.disableTimeSeriesChanges = false; - this.selectionTimeout = null; - } - - filteredPort = () => { - const urlParams = new URLSearchParams(this.props.location.search); - return urlParams.get("service_port"); - }; - - componentDidMount() { - const filteredPort = this.filteredPort(); - this.setState({filteredPort}); - this.loadStatistics(this.state.metric, filteredPort).then(() => log.debug("Statistics loaded after mount")); - - dispatcher.register("connection_updates", payload => { - this.setState({ - selection: new TimeRange(payload.from, payload.to), - }); - }); - - dispatcher.register("notifications", payload => { - if (payload.event === "services.edit") { - this.loadServices().then(() => log.debug("Services reloaded after notification update")); - } - }); - } - - componentDidUpdate(prevProps, prevState, snapshot) { - const filteredPort = this.filteredPort(); - if (this.state.filteredPort !== filteredPort) { - this.setState({filteredPort}); - this.loadStatistics(this.state.metric, filteredPort).then(() => - log.debug("Statistics reloaded after filtered port changes")); - } - } - - loadStatistics = async (metric, filteredPort) => { - const urlParams = new URLSearchParams(); - urlParams.set("metric", metric); - - let services = await this.loadServices(); - if (filteredPort && services[filteredPort]) { - const service = services[filteredPort]; - services = {}; - services[filteredPort] = service; - } - - const ports = Object.keys(services); - ports.forEach(s => urlParams.append("ports", s)); - - const metrics = (await backend.get("/api/statistics?" + urlParams)).json; - const zeroFilledMetrics = []; - const toTime = m => new Date(m["range_start"]).getTime(); - - if (metrics.length > 0) { - let i = 0; - for (let interval = toTime(metrics[0]); interval <= toTime(metrics[metrics.length - 1]); interval += minutes) { - if (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] = {}; - ports.forEach(p => m[metric][p] = 0); - zeroFilledMetrics.push(m); - } - } - } - - const series = new TimeSeries({ - name: "statistics", - columns: ["time"].concat(ports), - points: zeroFilledMetrics.map(m => [m["range_start"]].concat(ports.map(p => m[metric][p] || 0))) - }); - const start = series.range().begin(); - const end = series.range().end(); - start.setTime(start.getTime() - minutes); - end.setTime(end.getTime() + minutes); - - this.setState({ - metric, - series, - timeRange: new TimeRange(start, end), - start, - end - }); - log.debug(`Loaded statistics for metric "${metric}" for services [${ports}]`); - }; - - loadServices = async () => { - const services = (await backend.get("/api/services")).json; - this.setState({services}); - return services; - }; - - createStyler = () => { - 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); - }; - - 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 ( -
-
- - - - - - - - - - - - - - -
- this.loadStatistics(metric, this.state.filteredPort) - .then(() => log.debug("Statistics loaded after metric changes"))} - value={this.state.metric}/> -
-
-
- ); - } -} - -export default withRouter(Timeline); diff --git a/frontend/src/views/Timeline.scss b/frontend/src/views/Timeline.scss deleted file mode 100644 index 14360d4..0000000 --- a/frontend/src/views/Timeline.scss +++ /dev/null @@ -1,22 +0,0 @@ -@import "../colors.scss"; - -.footer { - padding: 15px; - - .time-line { - position: relative; - background-color: $color-primary-0; - - .metric-selection { - font-size: 0.8em; - position: absolute; - top: 5px; - right: 10px; - } - } - - svg text { - font-family: "Fira Code", monospace !important; - fill: $color-primary-4 !important; - } -} -- cgit v1.2.3-70-g09d2 From bc989facca1f3381afed2f7c982da7784fad2327 Mon Sep 17 00:00:00 2001 From: Emiliano Ciavatta Date: Sun, 11 Oct 2020 21:49:40 +0200 Subject: [inconsistent] SearchOption validation checkpoint --- application_context.go | 2 + application_router.go | 70 +++++++++++++++ connection_streams_controller.go | 1 + search_controller.go | 189 +++++++++++++++++++++++++++++++++++++++ statistics_controller.go | 8 +- storage.go | 41 ++++++--- stream_handler.go | 2 + 7 files changed, 299 insertions(+), 14 deletions(-) create mode 100644 search_controller.go (limited to 'connection_streams_controller.go') diff --git a/application_context.go b/application_context.go index 9897bb6..fc76d00 100644 --- a/application_context.go +++ b/application_context.go @@ -37,6 +37,7 @@ type ApplicationContext struct { ConnectionsController ConnectionsController ServicesController *ServicesController ConnectionStreamsController ConnectionStreamsController + SearchController *SearchController StatisticsController StatisticsController IsConfigured bool Version string @@ -113,6 +114,7 @@ func (sm *ApplicationContext) configure() { sm.ServicesController = NewServicesController(sm.Storage) sm.ConnectionsController = NewConnectionsController(sm.Storage, sm.ServicesController) sm.ConnectionStreamsController = NewConnectionStreamsController(sm.Storage) + sm.SearchController = NewSearchController(sm.Storage) sm.StatisticsController = NewStatisticsController(sm.Storage) sm.IsConfigured = true } diff --git a/application_router.go b/application_router.go index 89b471b..dc9f9d4 100644 --- a/application_router.go +++ b/application_router.go @@ -22,6 +22,8 @@ import ( "fmt" "github.com/gin-gonic/contrib/static" "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" + "github.com/go-playground/validator/v10" log "github.com/sirupsen/logrus" "net/http" "os" @@ -294,6 +296,74 @@ func CreateApplicationRouter(applicationContext *ApplicationContext, } }) + api.GET("/searches", func(c *gin.Context) { + success(c, applicationContext.SearchController.PerformedSearches()) + }) + + api.POST("/searches/perform", func(c *gin.Context) { + var options SearchOptions + + + parentIsZero := func (fl validator.FieldLevel) bool { + log.Println(fl.FieldName()) + log.Println("noooooo") + return fl.Parent().IsZero() + } + eitherWith := func (fl validator.FieldLevel) bool { + otherField := fl.Parent().FieldByName(fl.Param()) + log.Println(fl.Param()) + log.Println("bbbbbbbbbb") + return (fl.Field().IsZero() && !otherField.IsZero()) || (!fl.Field().IsZero() && otherField.IsZero()) + } + aaa := func (fl validator.FieldLevel) bool { + + log.Println("awww") + return fl.Field().IsZero() + } + + bbb := func (fl validator.FieldLevel) bool { + + log.Println("iiiii") + return true + } + + if validate, ok := binding.Validator.Engine().(*validator.Validate); ok { + if err := validate.RegisterValidation("parent_is_zero", parentIsZero); err != nil { + log.WithError(err).Panic("cannot register 'topzero' validator") + } + if err := validate.RegisterValidation("either_with", eitherWith); err != nil { + log.WithError(err).Panic("cannot register 'either_with' validator") + } + if err := validate.RegisterValidation("aaa", aaa); err != nil { + log.WithError(err).Panic("cannot register 'either_with' validator") + } + if err := validate.RegisterValidation("bbb", bbb); err != nil { + log.WithError(err).Panic("cannot register 'either_with' validator") + } + } else { + log.Panic("cannot ") + } + + + if err := c.ShouldBindJSON(&options); err != nil { + badRequest(c, err) + return + } + + log.Println(options) + + + success(c, "ok") + + + + + + + + //success(c, applicationContext.SearchController.PerformSearch(c, options)) + }) + api.GET("/streams/:id", func(c *gin.Context) { id, err := RowIDFromHex(c.Param("id")) if err != nil { diff --git a/connection_streams_controller.go b/connection_streams_controller.go index 89e484d..eef1a2a 100644 --- a/connection_streams_controller.go +++ b/connection_streams_controller.go @@ -39,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"` diff --git a/search_controller.go b/search_controller.go new file mode 100644 index 0000000..ad47dbc --- /dev/null +++ b/search_controller.go @@ -0,0 +1,189 @@ +/* + * 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 . + */ + +package main + +import ( + "context" + log "github.com/sirupsen/logrus" + "strings" + "sync" + "time" +) + +const ( + secondsToNano = 1000 * 1000 * 1000 + maxSearchTimeout = 60 * secondsToNano +) + +type PerformedSearch struct { + ID RowID `bson:"_id" json:"id"` + SearchOptions SearchOptions `bson:"search_options" json:"search_options"` + AffectedConnections []RowID `bson:"affected_connections" json:"affected_connections,omitempty"` + 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" validate:"either_with=RegexSearch"` + RegexSearch RegexSearch `bson:"regex_search" json:"regex_search" validate:"either_with=TextSearch"` + Timeout time.Duration `bson:"timeout" json:"timeout" binding:"max=60"` +} + +type TextSearch struct { + Terms []string `bson:"terms" json:"terms" binding:"parent_is_zero|either_with=ExactPhrase,isdefault|min=1,dive,min=3"` + ExcludedTerms []string `bson:"excluded_terms" json:"excluded_terms" binding:"required_with=Terms,dive,isdefault|min=1"` + ExactPhrase string `bson:"exact_phrase" json:"exact_phrase" binding:"isdefault|min=3,parent_is_zero|either_with=Terms"` + CaseSensitive bool `bson:"case_sensitive" json:"case_sensitive"` +} + +type RegexSearch struct { + Pattern string `bson:"pattern" json:"pattern" binding:"parent_is_zero|either_with=NotPattern,isdefault|min=3"` + NotPattern string `bson:"not_pattern" json:"not_pattern" binding:"parent_is_zero|either_with=Pattern,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).All(&searches); err != nil { + // log.WithError(err).Panic("failed to retrieve performed searches") + } + + return &SearchController{ + storage: storage, + performedSearches: searches, + } +} + +func (sc *SearchController) PerformedSearches() []PerformedSearch { + sc.mutex.Lock() + defer sc.mutex.Unlock() + + return sc.performedSearches +} + +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...) + 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/statistics_controller.go b/statistics_controller.go index 006b230..1714c0b 100644 --- a/statistics_controller.go +++ b/statistics_controller.go @@ -26,10 +26,10 @@ import ( 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"` - DurationPerService map[uint16]int64 `json:"duration_per_service" bson:"duration_per_service"` + ConnectionsPerService map[uint16]int `json:"connections_per_service,omitempty" bson:"connections_per_service"` + ClientBytesPerService map[uint16]int `json:"client_bytes_per_service,omitempty" bson:"client_bytes_per_service"` + ServerBytesPerService map[uint16]int `json:"server_bytes_per_service,omitempty" bson:"server_bytes_per_service"` + DurationPerService map[uint16]int64 `json:"duration_per_service,omitempty" bson:"duration_per_service"` } type StatisticsFilter struct { diff --git a/storage.go b/storage.go index f8b7f9c..304e88c 100644 --- a/storage.go +++ b/storage.go @@ -25,16 +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 Statistics = "statistics" +const ( + Connections = "connections" + ConnectionStreams = "connection_streams" + ImportingSessions = "importing_sessions" + Rules = "rules" + Searches = "searches" + Settings = "settings" + Services = "services" + Statistics = "statistics" +) var ZeroRowID [12]byte @@ -73,6 +77,7 @@ 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(Services), Settings: db.Collection(Settings), Services: db.Collection(Services), Statistics: db.Collection(Statistics), @@ -85,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 } @@ -277,6 +286,8 @@ type FindOperation interface { 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 } @@ -318,6 +329,16 @@ func (fo MongoFindOperation) Limit(n int64) FindOperation { 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 { diff --git a/stream_handler.go b/stream_handler.go index 48dba34..f08bd70 100644 --- a/stream_handler.go +++ b/stream_handler.go @@ -22,6 +22,7 @@ import ( "github.com/flier/gohs/hyperscan" "github.com/google/gopacket/tcpassembly" log "github.com/sirupsen/logrus" + "strings" "time" ) @@ -176,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, -- cgit v1.2.3-70-g09d2 From 5534413b3a3e6e783310be8147ac8340d3098a7e Mon Sep 17 00:00:00 2001 From: Emiliano Ciavatta Date: Fri, 16 Oct 2020 15:09:05 +0200 Subject: Fix tests. General refactor --- application_context_test.go | 7 + connection_streams_controller.go | 4 +- frontend/src/backend.js | 2 +- frontend/src/components/Notifications.js | 2 +- frontend/src/components/Notifications.scss | 2 +- frontend/src/components/fields/ChoiceField.scss | 4 +- frontend/src/components/fields/InputField.scss | 8 +- frontend/src/components/fields/TagField.scss | 26 ++-- frontend/src/components/filters/AdvancedFilters.js | 2 +- .../components/filters/RulesConnectionsFilter.js | 14 +- .../components/filters/StringConnectionsFilter.js | 8 +- frontend/src/components/pages/ConfigurationPage.js | 18 +-- frontend/src/components/panels/ConnectionsPane.js | 11 +- frontend/src/components/panels/PcapsPane.js | 10 +- frontend/src/components/panels/RulesPane.js | 90 +++++++------ frontend/src/components/panels/SearchPane.js | 10 +- frontend/src/components/panels/StreamsPane.js | 4 +- frontend/src/index.js | 3 - frontend/src/index.scss | 2 + frontend/src/log.js | 7 +- frontend/src/serviceWorker.js | 141 --------------------- frontend/src/setupTests.js | 5 - frontend/src/utils.js | 2 +- parsers/http_request_parser.go | 28 ++-- parsers/http_response_parser.go | 12 +- parsers/parser.go | 4 +- pcap_importer_test.go | 1 + rules_manager.go | 4 +- scripts/generate_ping.py | 2 +- utils.go | 8 +- 30 files changed, 150 insertions(+), 291 deletions(-) delete mode 100644 frontend/src/serviceWorker.js delete mode 100644 frontend/src/setupTests.js (limited to 'connection_streams_controller.go') diff --git a/application_context_test.go b/application_context_test.go index a7f1a49..11d1ed4 100644 --- a/application_context_test.go +++ b/application_context_test.go @@ -35,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}", @@ -58,11 +62,14 @@ func TestCreateApplicationContext(t *testing.T) { 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/connection_streams_controller.go b/connection_streams_controller.go index eef1a2a..038c2c5 100644 --- a/connection_streams_controller.go +++ b/connection_streams_controller.go @@ -369,7 +369,7 @@ func decodePwntools(payload []byte, isClient bool, format string) string { if isClient { return fmt.Sprintf("p.send(%s)\n", content) - } else { - return fmt.Sprintf("p.recvuntil(%s)\n", content) } + + return fmt.Sprintf("p.recvuntil(%s)\n", content) } diff --git a/frontend/src/backend.js b/frontend/src/backend.js index cc8604a..dc5089f 100644 --- a/frontend/src/backend.js +++ b/frontend/src/backend.js @@ -17,7 +17,7 @@ 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", diff --git a/frontend/src/components/Notifications.js b/frontend/src/components/Notifications.js index 92731d9..a81eba1 100644 --- a/frontend/src/components/Notifications.js +++ b/frontend/src/components/Notifications.js @@ -67,7 +67,7 @@ class Notifications extends Component { n.variant = "blue"; return this.pushNotification(n); default: - return; + return null; } }; diff --git a/frontend/src/components/Notifications.scss b/frontend/src/components/Notifications.scss index 98d228e..5852c7d 100644 --- a/frontend/src/components/Notifications.scss +++ b/frontend/src/components/Notifications.scss @@ -32,7 +32,7 @@ } &.notification-open { - transform: translateX(0px); + transform: translateX(0); } &.notification-closed { diff --git a/frontend/src/components/fields/ChoiceField.scss b/frontend/src/components/fields/ChoiceField.scss index c8c7ff1..85986af 100644 --- a/frontend/src/components/fields/ChoiceField.scss +++ b/frontend/src/components/fields/ChoiceField.scss @@ -27,8 +27,8 @@ } .field-options { - position: absolute; - z-index: 20; + position: static; + z-index: 100; top: 35px; display: none; width: 100%; diff --git a/frontend/src/components/fields/InputField.scss b/frontend/src/components/fields/InputField.scss index e8ef46a..eafb2ab 100644 --- a/frontend/src/components/fields/InputField.scss +++ b/frontend/src/components/fields/InputField.scss @@ -47,15 +47,15 @@ background-color: $color-primary-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; } - - .file-label::after { - background-color: $color-secondary-4 !important; - } } &.field-invalid { diff --git a/frontend/src/components/fields/TagField.scss b/frontend/src/components/fields/TagField.scss index 737f11f..723e71f 100644 --- a/frontend/src/components/fields/TagField.scss +++ b/frontend/src/components/fields/TagField.scss @@ -10,6 +10,18 @@ } } + .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; } @@ -39,18 +51,6 @@ } } - .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; - } - } - .react-tags__selected { display: inline-block; flex: 0 1; @@ -154,4 +154,4 @@ cursor: auto; opacity: 0.5; } -} \ No newline at end of file +} diff --git a/frontend/src/components/filters/AdvancedFilters.js b/frontend/src/components/filters/AdvancedFilters.js index 2b479ed..15667a5 100644 --- a/frontend/src/components/filters/AdvancedFilters.js +++ b/frontend/src/components/filters/AdvancedFilters.js @@ -33,7 +33,7 @@ class AdvancedFilters extends Component { 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: active}); + this.setState({active}); } }; dispatcher.register("connections_filters", this.connectionsFiltersCallback); diff --git a/frontend/src/components/filters/RulesConnectionsFilter.js b/frontend/src/components/filters/RulesConnectionsFilter.js index 86eae7e..37d36b7 100644 --- a/frontend/src/components/filters/RulesConnectionsFilter.js +++ b/frontend/src/components/filters/RulesConnectionsFilter.js @@ -35,17 +35,17 @@ class RulesConnectionsFilter extends Component { 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)); + 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}); }); - this.connectionsFiltersCallback = payload => { + 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)); + const newRules = this.state.rules.filter((r) => payload["matched_rules"].includes(r.id)); this.setState({ - activeRules: newRules.map(r => { + activeRules: newRules.map((r) => { return {id: r.id, name: r.name}; }) }); @@ -61,7 +61,7 @@ class RulesConnectionsFilter extends Component { onChange = (activeRules) => { if (!_.isEqual(activeRules.sort(), this.state.activeRules.sort())) { this.setState({activeRules}); - dispatcher.dispatch("connections_filters", {"matched_rules": activeRules.map(r => r.id)}); + dispatcher.dispatch("connections_filters", {"matched_rules": activeRules.map((r) => r.id)}); } }; diff --git a/frontend/src/components/filters/StringConnectionsFilter.js b/frontend/src/components/filters/StringConnectionsFilter.js index c3c5925..c5d7075 100644 --- a/frontend/src/components/filters/StringConnectionsFilter.js +++ b/frontend/src/components/filters/StringConnectionsFilter.js @@ -33,7 +33,7 @@ class StringConnectionsFilter extends Component { let params = new URLSearchParams(this.props.location.search); this.updateStateFromFilterValue(params.get(this.props.filterName)); - this.connectionsFiltersCallback = payload => { + this.connectionsFiltersCallback = (payload) => { const name = this.props.filterName; if (name in payload && this.state.filterValue !== payload[name]) { this.updateStateFromFilterValue(payload[name]); @@ -56,7 +56,7 @@ class StringConnectionsFilter extends Component { fieldValue = this.props.replaceFunc(fieldValue); } if (this.isValueValid(fieldValue)) { - this.setState({fieldValue, filterValue: filterValue}); + this.setState({fieldValue, filterValue}); } else { this.setState({fieldValue, invalidValue: true}); } @@ -98,9 +98,9 @@ class StringConnectionsFilter extends Component { } this.setState({ - fieldValue: fieldValue, + fieldValue, timeoutHandle: setTimeout(() => { - this.setState({filterValue: filterValue}); + this.setState({filterValue}); this.changeFilterValue(filterValue); }, 500), invalidValue: false diff --git a/frontend/src/components/pages/ConfigurationPage.js b/frontend/src/components/pages/ConfigurationPage.js index 2bd2da7..4f0ce21 100644 --- a/frontend/src/components/pages/ConfigurationPage.js +++ b/frontend/src/components/pages/ConfigurationPage.js @@ -59,11 +59,11 @@ class ConfigurationPage 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; } @@ -84,7 +84,7 @@ class ConfigurationPage extends Component { this.setState({ newUsername: "", newPassword: "", - settings: settings + settings }); } else { this.setState({ @@ -128,15 +128,15 @@ class ConfigurationPage extends Component { - this.updateParam((s) => s.config.server_address = v)}/> - this.updateParam((s) => s.config.flag_regex = v)} + onChange={(v) => this.updateParam((s) => s.config["server_address"] = v)}/> + this.updateParam((s) => s.config["flag_regex"] = v)} error={this.state.flagRegexError}/>
- this.updateParam((s) => s.config.auth_required = v)}/> + this.updateParam((s) => s.config["auth_required"] = v)}/>
diff --git a/frontend/src/components/panels/ConnectionsPane.js b/frontend/src/components/panels/ConnectionsPane.js index 23c6114..9418fad 100644 --- a/frontend/src/components/panels/ConnectionsPane.js +++ b/frontend/src/components/panels/ConnectionsPane.js @@ -178,7 +178,7 @@ class ConnectionsPane extends Component { let firstConnection = this.state.firstConnection; let lastConnection = this.state.lastConnection; - if (additionalParams !== undefined && additionalParams.from !== undefined && additionalParams.to === undefined) { + if (additionalParams && additionalParams.from && !additionalParams.to) { if (res.length > 0) { if (!isInitial) { res = res.slice(1); @@ -194,7 +194,7 @@ class ConnectionsPane extends Component { firstConnection = connections[0]; } } - } else if (additionalParams !== undefined && additionalParams.to !== undefined && additionalParams.from === undefined) { + } 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]; @@ -215,12 +215,7 @@ class ConnectionsPane extends Component { } } - this.setState({ - loading: false, - connections: connections, - firstConnection: firstConnection, - lastConnection: lastConnection - }); + this.setState({loading: false, connections, firstConnection, lastConnection}); if (firstConnection != null && lastConnection != null) { dispatcher.dispatch("connection_updates", { diff --git a/frontend/src/components/panels/PcapsPane.js b/frontend/src/components/panels/PcapsPane.js index 64e7804..ddc5948 100644 --- a/frontend/src/components/panels/PcapsPane.js +++ b/frontend/src/components/panels/PcapsPane.js @@ -181,15 +181,15 @@ class PcapsPane extends Component { }; const uploadCurlCommand = createCurlCommand("/pcap/upload", "POST", null, { - file: "@" + ((this.state.uploadSelectedFile != null && this.state.isUploadFileValid) ? + "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 + "file": this.state.fileValue, + "flush_all": this.state.processFlushAll, + "delete_original_file": this.state.deleteOriginalFile }); return ( diff --git a/frontend/src/components/panels/RulesPane.js b/frontend/src/components/panels/RulesPane.js index d872b47..0bbd407 100644 --- a/frontend/src/components/panels/RulesPane.js +++ b/frontend/src/components/panels/RulesPane.js @@ -142,23 +142,23 @@ class RulesPane 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; } @@ -175,9 +175,9 @@ class RulesPane 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, @@ -210,9 +210,7 @@ class RulesPane extends Component { const newPattern = _.cloneDeep(this.emptyPattern); this.currentRule().patterns.push(pattern); - this.setState({ - newPattern: newPattern - }); + this.setState({newPattern}); }; editPattern = (pattern) => { @@ -237,7 +235,7 @@ class RulesPane 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}); } @@ -273,25 +271,25 @@ class RulesPane extends Component { this.setState({patternRegexFocused: pattern.regex === ""}); }}/> - this.updateParam(() => pattern.flags.caseless = v)}/> - this.updateParam(() => pattern.flags.dot_all = v)}/> - this.updateParam(() => pattern.flags.multi_line = v)}/> - this.updateParam(() => pattern.flags.utf_8_mode = v)}/> - this.updateParam(() => pattern.flags.unicode_property = v)}/> + this.updateParam(() => pattern.flags["caseless"] = v)}/> + this.updateParam(() => pattern.flags["dot_all"] = v)}/> + this.updateParam(() => pattern.flags["multi_line"] = v)}/> + this.updateParam(() => pattern.flags["utf_8_mode"] = v)}/> + this.updateParam(() => pattern.flags["unicode_property"] = v)}/> - this.updateParam(() => pattern.min_occurrences = v)}/> + onChange={(v) => this.updateParam(() => pattern["min_occurrences"] = v)}/> - this.updateParam(() => pattern.max_occurrences = v)}/> + onChange={(v) => this.updateParam(() => pattern["max_occurrences"] = v)}/> s", "s->c"]} value={this.directions[pattern.direction]} @@ -305,13 +303,13 @@ class RulesPane extends Component { : {p.regex} - {p.flags.caseless ? "yes" : "no"} - {p.flags.dot_all ? "yes" : "no"} - {p.flags.multi_line ? "yes" : "no"} - {p.flags.utf_8_mode ? "yes" : "no"} - {p.flags.unicode_property ? "yes" : "no"} - {p.min_occurrences} - {p.max_occurrences} + {p.flags["caseless"] ? "yes" : "no"} + {p.flags["dot_all"] ? "yes" : "no"} + {p.flags["multi_line"] ? "yes" : "no"} + {p.flags["utf_8_mode"] ? "yes" : "no"} + {p.flags["unicode_property"] ? "yes" : "no"} + {p["min_occurrences"]} + {p["max_occurrences"]} {this.directions[p.direction]} {!isUpdate && this.editPattern(p)}/>} @@ -373,32 +371,32 @@ class RulesPane extends Component { filters: - this.updateParam((r) => r.filter.service_port = v)} + this.updateParam((r) => r.filter["service_port"] = v)} min={0} max={65565} error={this.state.ruleServicePortError} readonly={isUpdate}/> - this.updateParam((r) => r.filter.client_port = v)} + this.updateParam((r) => r.filter["client_port"] = v)} min={0} max={65565} error={this.state.ruleClientPortError} readonly={isUpdate}/> - this.updateParam((r) => r.filter.client_address = v)}/> + onChange={(v) => this.updateParam((r) => r.filter["client_address"] = v)}/> - this.updateParam((r) => r.filter.min_duration = v)}/> - this.updateParam((r) => r.filter["min_duration"] = v)}/> + this.updateParam((r) => r.filter.max_duration = v)}/> - this.updateParam((r) => r.filter["max_duration"] = v)}/> + this.updateParam((r) => r.filter.min_bytes = v)}/> - this.updateParam((r) => r.filter["min_bytes"] = v)}/> + this.updateParam((r) => r.filter.max_bytes = v)}/> + onChange={(v) => this.updateParam((r) => r.filter["max_bytes"] = v)}/>
diff --git a/frontend/src/components/panels/SearchPane.js b/frontend/src/components/panels/SearchPane.js index d36e85e..776ebd0 100644 --- a/frontend/src/components/panels/SearchPane.js +++ b/frontend/src/components/panels/SearchPane.js @@ -70,20 +70,20 @@ class SearchPane extends Component { 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)})); + .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 => { + 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 => { + }).catch((res) => { this.setState({ searchStatusCode: res.status, searchResponse: JSON.stringify(res.json), loading: false @@ -168,7 +168,7 @@ class SearchPane extends Component { render() { const options = this.state.currentSearchOptions; - let searches = this.state.searches.map(s => + let searches = this.state.searches.map((s) => {s.id.substring(0, 8)} {this.extractPattern(s["search_options"])} diff --git a/frontend/src/components/panels/StreamsPane.js b/frontend/src/components/panels/StreamsPane.js index 41ab33d..1819fec 100644 --- a/frontend/src/components/panels/StreamsPane.js +++ b/frontend/src/components/panels/StreamsPane.js @@ -18,7 +18,7 @@ import DOMPurify from "dompurify"; import React, {Component} from "react"; import {Row} from "react-bootstrap"; -import ReactJson from "react-json-view" +import ReactJson from "react-json-view"; import backend from "../../backend"; import log from "../../log"; import {downloadBlob, getHeaderValue} from "../../utils"; @@ -72,7 +72,7 @@ class StreamsPane extends Component { setFormat = (format) => { if (this.validFormats.includes(format)) { - this.setState({format: format}); + this.setState({format}); } }; diff --git a/frontend/src/index.js b/frontend/src/index.js index d00df88..62cb974 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -21,7 +21,6 @@ import ReactDOM from "react-dom"; import App from "./components/App"; import "./index.scss"; import notifications from "./notifications"; -import * as serviceWorker from "./serviceWorker"; notifications.createWebsocket(); @@ -31,5 +30,3 @@ ReactDOM.render( // , document.getElementById("root") ); - -serviceWorker.unregister(); diff --git a/frontend/src/index.scss b/frontend/src/index.scss index ea360be..1378d81 100644 --- a/frontend/src/index.scss +++ b/frontend/src/index.scss @@ -82,9 +82,11 @@ a { 0% { opacity: 1; } + 50% { opacity: 0.3; } + 100% { opacity: 1; } diff --git a/frontend/src/log.js b/frontend/src/log.js index 424e1b4..0afac47 100644 --- a/frontend/src/log.js +++ b/frontend/src/log.js @@ -16,9 +16,14 @@ */ const log = { - debug: (...obj) => console.info(...obj), + 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/serviceWorker.js b/frontend/src/serviceWorker.js deleted file mode 100644 index a1f0ba8..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/setupTests.js b/frontend/src/setupTests.js deleted file mode 100644 index 9b1d6d0..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"; \ No newline at end of file diff --git a/frontend/src/utils.js b/frontend/src/utils.js index 0f0927e..445e576 100644 --- a/frontend/src/utils.js +++ b/frontend/src/utils.js @@ -133,7 +133,7 @@ export function getHeaderValue(request, key) { if (request && request.headers) { return request.headers[Object.keys(request.headers).find((k) => k.toLowerCase() === key.toLowerCase())]; } - return undefined; + return null; } export function downloadBlob(blob, fileName) { diff --git a/parsers/http_request_parser.go b/parsers/http_request_parser.go index 56093c9..d7ed81c 100644 --- a/parsers/http_request_parser.go +++ b/parsers/http_request_parser.go @@ -28,7 +28,7 @@ import ( "strings" ) -type HttpRequestMetadata struct { +type HTTPRequestMetadata struct { BasicMetadata Method string `json:"method"` URL string `json:"url"` @@ -40,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 { @@ -68,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(), @@ -80,7 +80,7 @@ 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, body), FetchRequest: fetchRequest(request, body), @@ -92,17 +92,17 @@ 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, body string) string { var b strings.Builder - 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() + "\"") @@ -146,14 +146,14 @@ 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" } } -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 e5ef1ac..e61fffd 100644 --- a/parsers/http_response_parser.go +++ b/parsers/http_response_parser.go @@ -26,7 +26,7 @@ import ( "net/http" ) -type HttpResponseMetadata struct { +type HTTPResponseMetadata struct { BasicMetadata Status string `json:"status"` StatusCode int `json:"status_code"` @@ -40,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 { @@ -74,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 a29b1ab..9aca3b6 100644 --- a/parsers/parser.go +++ b/parsers/parser.go @@ -30,8 +30,8 @@ type BasicMetadata struct { } var parsers = []Parser{ // order matter - HttpRequestParser{}, - HttpResponseParser{}, + HTTPRequestParser{}, + HTTPResponseParser{}, } func Parse(content []byte) Metadata { diff --git a/pcap_importer_test.go b/pcap_importer_test.go index 8940060..a47f2d9 100644 --- a/pcap_importer_test.go +++ b/pcap_importer_test.go @@ -127,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/rules_manager.go b/rules_manager.go index 327e4ec..a6d969f 100644 --- a/rules_manager.go +++ b/rules_manager.go @@ -328,10 +328,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 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/utils.go b/utils.go index 639fd94..e721d78 100644 --- a/utils.go +++ b/utils.go @@ -57,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 { -- cgit v1.2.3-70-g09d2