aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEmiliano Ciavatta2020-10-07 12:58:48 +0000
committerEmiliano Ciavatta2020-10-07 12:58:48 +0000
commitd5f94b76986615b255b77b2a7b7ed336e5ad4838 (patch)
treec813c55845be273efccf60995f43a77fdee68ac8
parente905618113309eaba7227ff1328a20f6846e4afd (diff)
Implement notifications
-rw-r--r--VERSION1
-rw-r--r--application_context.go10
-rw-r--r--application_context_test.go4
-rw-r--r--application_router.go27
-rw-r--r--application_router_test.go6
-rw-r--r--caronte.go12
-rw-r--r--frontend/package.json5
-rw-r--r--frontend/src/backend.js3
-rw-r--r--frontend/src/components/Connection.js11
-rw-r--r--frontend/src/components/ConnectionContent.scss9
-rw-r--r--frontend/src/components/Notifications.js60
-rw-r--r--frontend/src/components/Notifications.scss48
-rw-r--r--frontend/src/components/panels/PcapPane.js33
-rw-r--r--frontend/src/components/panels/RulePane.js116
-rw-r--r--frontend/src/components/panels/ServicePane.js35
-rw-r--r--frontend/src/components/panels/common.scss4
-rw-r--r--frontend/src/dispatcher.js35
-rw-r--r--frontend/src/globals.js5
-rw-r--r--frontend/src/index.js6
-rw-r--r--frontend/src/notifications.js40
-rw-r--r--frontend/src/setupProxy.js7
-rw-r--r--frontend/src/views/App.js43
-rw-r--r--frontend/src/views/Connections.js57
-rw-r--r--frontend/src/views/Connections.scss3
-rw-r--r--frontend/src/views/Footer.js20
-rw-r--r--frontend/yarn.lock32
-rw-r--r--go.mod1
-rw-r--r--go.sum2
-rw-r--r--notification_controller.go165
-rw-r--r--pcap_importer.go7
30 files changed, 624 insertions, 183 deletions
diff --git a/VERSION b/VERSION
new file mode 100644
index 0000000..ce609ca
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+0.8 \ No newline at end of file
diff --git a/application_context.go b/application_context.go
index 6ea449d..9a9c97a 100644
--- a/application_context.go
+++ b/application_context.go
@@ -22,9 +22,10 @@ type ApplicationContext struct {
ConnectionStreamsController ConnectionStreamsController
StatisticsController StatisticsController
IsConfigured bool
+ Version string
}
-func CreateApplicationContext(storage Storage) (*ApplicationContext, error) {
+func CreateApplicationContext(storage Storage, version string) (*ApplicationContext, error) {
var configWrapper struct {
Config Config
}
@@ -45,9 +46,10 @@ func CreateApplicationContext(storage Storage) (*ApplicationContext, error) {
}
applicationContext := &ApplicationContext{
- Storage: storage,
- Config: configWrapper.Config,
- Accounts: accountsWrapper.Accounts,
+ Storage: storage,
+ Config: configWrapper.Config,
+ Accounts: accountsWrapper.Accounts,
+ Version: version,
}
applicationContext.configure()
diff --git a/application_context_test.go b/application_context_test.go
index eed0fd6..28c81a5 100644
--- a/application_context_test.go
+++ b/application_context_test.go
@@ -10,7 +10,7 @@ func TestCreateApplicationContext(t *testing.T) {
wrapper := NewTestStorageWrapper(t)
wrapper.AddCollection(Settings)
- appContext, err := CreateApplicationContext(wrapper.Storage)
+ appContext, err := CreateApplicationContext(wrapper.Storage, "test")
assert.NoError(t, err)
assert.False(t, appContext.IsConfigured)
assert.Zero(t, appContext.Config)
@@ -39,7 +39,7 @@ func TestCreateApplicationContext(t *testing.T) {
appContext.SetConfig(config)
appContext.SetAccounts(accounts)
- checkAppContext, err := CreateApplicationContext(wrapper.Storage)
+ checkAppContext, err := CreateApplicationContext(wrapper.Storage, "test")
assert.NoError(t, err)
assert.True(t, checkAppContext.IsConfigured)
assert.Equal(t, checkAppContext.Config, config)
diff --git a/application_router.go b/application_router.go
index 8b5e32f..6431e22 100644
--- a/application_router.go
+++ b/application_router.go
@@ -13,7 +13,8 @@ import (
"time"
)
-func CreateApplicationRouter(applicationContext *ApplicationContext) *gin.Engine {
+func CreateApplicationRouter(applicationContext *ApplicationContext,
+ notificationController *NotificationController) *gin.Engine {
router := gin.New()
router.Use(gin.Logger())
router.Use(gin.Recovery())
@@ -47,6 +48,13 @@ func CreateApplicationRouter(applicationContext *ApplicationContext) *gin.Engine
applicationContext.SetAccounts(settings.Accounts)
c.JSON(http.StatusAccepted, gin.H{})
+ notificationController.Notify("setup", InsertNotification, gin.H{})
+ })
+
+ router.GET("/ws", func(c *gin.Context) {
+ if err := notificationController.NotificationHandler(c.Writer, c.Request); err != nil {
+ serverError(c, err)
+ }
})
api := router.Group("/api")
@@ -68,7 +76,9 @@ func CreateApplicationRouter(applicationContext *ApplicationContext) *gin.Engine
if id, err := applicationContext.RulesManager.AddRule(c, rule); err != nil {
unprocessableEntity(c, err)
} else {
- success(c, UnorderedDocument{"id": id})
+ response := UnorderedDocument{"id": id}
+ success(c, response)
+ notificationController.Notify("rules.new", InsertNotification, response)
}
})
@@ -107,6 +117,7 @@ func CreateApplicationRouter(applicationContext *ApplicationContext) *gin.Engine
notFound(c, UnorderedDocument{"id": id})
} else {
success(c, rule)
+ notificationController.Notify("rules.edit", UpdateNotification, rule)
}
})
@@ -126,7 +137,9 @@ func CreateApplicationRouter(applicationContext *ApplicationContext) *gin.Engine
if sessionID, err := applicationContext.PcapImporter.ImportPcap(fileName, flushAll); err != nil {
unprocessableEntity(c, err)
} else {
- c.JSON(http.StatusAccepted, gin.H{"session": sessionID})
+ response := gin.H{"session": sessionID}
+ c.JSON(http.StatusAccepted, response)
+ notificationController.Notify("pcap.upload", InsertNotification, response)
}
})
@@ -158,7 +171,9 @@ func CreateApplicationRouter(applicationContext *ApplicationContext) *gin.Engine
}
unprocessableEntity(c, err)
} else {
- c.JSON(http.StatusAccepted, gin.H{"session": sessionID})
+ response := gin.H{"session": sessionID}
+ c.JSON(http.StatusAccepted, response)
+ notificationController.Notify("pcap.file", InsertNotification, response)
}
})
@@ -195,6 +210,7 @@ func CreateApplicationRouter(applicationContext *ApplicationContext) *gin.Engine
session := gin.H{"session": sessionID}
if cancelled := applicationContext.PcapImporter.CancelSession(sessionID); cancelled {
c.JSON(http.StatusAccepted, session)
+ notificationController.Notify("sessions.delete", DeleteNotification, session)
} else {
notFound(c, session)
}
@@ -254,6 +270,8 @@ func CreateApplicationRouter(applicationContext *ApplicationContext) *gin.Engine
if result {
c.Status(http.StatusAccepted)
+ notificationController.Notify("connections.action", UpdateNotification,
+ gin.H{"connection_id": c.Param("id"), "action": c.Param("action")})
} else {
notFound(c, gin.H{"connection": id})
}
@@ -285,6 +303,7 @@ func CreateApplicationRouter(applicationContext *ApplicationContext) *gin.Engine
}
if err := applicationContext.ServicesController.SetService(c, service); err == nil {
success(c, service)
+ notificationController.Notify("services.edit", UpdateNotification, service)
} else {
unprocessableEntity(c, err)
}
diff --git a/application_router_test.go b/application_router_test.go
index 4225ab9..f4804e3 100644
--- a/application_router_test.go
+++ b/application_router_test.go
@@ -148,10 +148,12 @@ func NewRouterTestToolkit(t *testing.T, withSetup bool) *RouterTestToolkit {
wrapper := NewTestStorageWrapper(t)
wrapper.AddCollection(Settings)
- appContext, err := CreateApplicationContext(wrapper.Storage)
+ appContext, err := CreateApplicationContext(wrapper.Storage, "test")
require.NoError(t, err)
gin.SetMode(gin.ReleaseMode)
- router := CreateApplicationRouter(appContext)
+ notificationController := NewNotificationController(appContext)
+ go notificationController.Run()
+ router := CreateApplicationRouter(appContext, notificationController)
toolkit := RouterTestToolkit{
appContext: appContext,
diff --git a/caronte.go b/caronte.go
index 098642c..288563c 100644
--- a/caronte.go
+++ b/caronte.go
@@ -4,6 +4,7 @@ import (
"flag"
"fmt"
log "github.com/sirupsen/logrus"
+ "io/ioutil"
)
func main() {
@@ -22,12 +23,19 @@ func main() {
log.WithError(err).WithFields(logFields).Fatal("failed to connect to MongoDB")
}
- applicationContext, err := CreateApplicationContext(storage)
+ versionBytes, err := ioutil.ReadFile("VERSION")
+ if err != nil {
+ log.WithError(err).Fatal("failed to load version file")
+ }
+
+ applicationContext, err := CreateApplicationContext(storage, string(versionBytes))
if err != nil {
log.WithError(err).WithFields(logFields).Fatal("failed to create application context")
}
- applicationRouter := CreateApplicationRouter(applicationContext)
+ notificationController := NewNotificationController(applicationContext)
+ go notificationController.Run()
+ applicationRouter := CreateApplicationRouter(applicationContext, notificationController)
if applicationRouter.Run(fmt.Sprintf("%s:%v", *bindAddress, *bindPort)) != nil {
log.WithError(err).WithFields(logFields).Fatal("failed to create the server")
}
diff --git a/frontend/package.json b/frontend/package.json
index 5bc13f1..b3ad03a 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -14,7 +14,7 @@
"classnames": "^2.2.6",
"dompurify": "^2.1.1",
"eslint-config-react-app": "^5.2.1",
- "flux": "^3.1.3",
+ "http-proxy-middleware": "^1.0.5",
"lodash": "^4.17.20",
"node-sass": "^4.14.0",
"pondjs": "^0.9.0",
@@ -50,6 +50,5 @@
"last 1 firefox version",
"last 1 safari version"
]
- },
- "proxy": "http://localhost:3333"
+ }
}
diff --git a/frontend/src/backend.js b/frontend/src/backend.js
index 72ee9dd..c7abd80 100644
--- a/frontend/src/backend.js
+++ b/frontend/src/backend.js
@@ -1,4 +1,3 @@
-
async function json(method, url, data, json, headers) {
const options = {
method: method,
@@ -28,7 +27,7 @@ async function json(method, url, data, json, headers) {
const backend = {
get: (url = "", headers = null) =>
- json("GET", url, null,null, headers),
+ json("GET", url, null, null, headers),
post: (url = "", data = null, headers = null) =>
json("POST", url, null, data, headers),
put: (url = "", data = null, headers = null) =>
diff --git a/frontend/src/components/Connection.js b/frontend/src/components/Connection.js
index 44f9f18..46a0cab 100644
--- a/frontend/src/components/Connection.js
+++ b/frontend/src/components/Connection.js
@@ -48,10 +48,11 @@ class Connection extends Component {
render() {
let conn = this.props.data;
let serviceName = "/dev/null";
- let serviceColor = "#0F192E";
- if (conn.service.port !== 0) {
- serviceName = conn.service.name;
- serviceColor = conn.service.color;
+ let 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);
@@ -87,7 +88,7 @@ class Connection extends Component {
<td>
<span className="connection-service">
<ButtonField small fullSpan color={serviceColor} name={serviceName}
- onClick={() => this.props.addServicePortFilter(conn.port_dst)} />
+ onClick={() => this.props.addServicePortFilter(conn.port_dst)}/>
</span>
</td>
<td className="clickable" onClick={this.props.onSelected}>{conn.ip_src}</td>
diff --git a/frontend/src/components/ConnectionContent.scss b/frontend/src/components/ConnectionContent.scss
index de4d699..f4edec9 100644
--- a/frontend/src/components/ConnectionContent.scss
+++ b/frontend/src/components/ConnectionContent.scss
@@ -2,7 +2,6 @@
.connection-content {
height: 100%;
- padding: 10px 10px 0;
background-color: $color-primary-0;
pre {
@@ -91,12 +90,12 @@
.connection-content-header {
height: 33px;
padding: 0;
- background-color: $color-primary-2;
+ background-color: $color-primary-3;
.header-info {
font-size: 12px;
padding-top: 7px;
- padding-left: 20px;
+ padding-left: 25px;
}
.header-actions {
@@ -104,6 +103,10 @@
.choice-field {
margin-top: -5px;
+
+ .field-value {
+ background-color: $color-primary-3;
+ }
}
}
}
diff --git a/frontend/src/components/Notifications.js b/frontend/src/components/Notifications.js
new file mode 100644
index 0000000..4d6dcd4
--- /dev/null
+++ b/frontend/src/components/Notifications.js
@@ -0,0 +1,60 @@
+import React, {Component} from 'react';
+import './Notifications.scss';
+import dispatcher from "../dispatcher";
+
+const _ = require('lodash');
+const classNames = require('classnames');
+
+class Notifications extends Component {
+
+ state = {
+ notifications: [],
+ closedNotifications: [],
+ };
+
+ componentDidMount() {
+ dispatcher.register("notifications", notification => {
+ const notifications = this.state.notifications;
+ notifications.push(notification);
+ this.setState({notifications});
+
+ setTimeout(() => {
+ const notifications = this.state.notifications;
+ notification.open = true;
+ this.setState({notifications});
+ }, 100);
+
+ setTimeout(() => {
+ const notifications = _.without(this.state.notifications, notification);
+ const closedNotifications = this.state.closedNotifications.concat([notification]);
+ notification.closed = true;
+ this.setState({notifications, closedNotifications});
+ }, 5000);
+
+ setTimeout(() => {
+ const closedNotifications = _.without(this.state.closedNotifications, notification);
+ this.setState({closedNotifications});
+ }, 6000);
+ });
+ }
+
+ render() {
+ return (
+ <div className="notifications">
+ <div className="notifications-list">
+ {
+ this.state.closedNotifications.concat(this.state.notifications).map(n =>
+ <div className={classNames("notification", {"notification-closed": n.closed},
+ {"notification-open": n.open})}>
+ <h3 className="notification-title">{n.event}</h3>
+ <span className="notification-description">{JSON.stringify(n.message)}</span>
+ </div>
+ )
+ }
+ </div>
+ </div>
+ );
+ }
+}
+
+export default Notifications;
diff --git a/frontend/src/components/Notifications.scss b/frontend/src/components/Notifications.scss
new file mode 100644
index 0000000..b0c334b
--- /dev/null
+++ b/frontend/src/components/Notifications.scss
@@ -0,0 +1,48 @@
+@import "../colors.scss";
+
+.notifications {
+ position: absolute;
+
+ left: 30px;
+ bottom: 50px;
+ z-index: 50;
+
+ .notifications-list {
+
+ }
+
+ .notification {
+ background-color: $color-green;
+ border-left: 5px solid $color-green-dark;
+ padding: 10px;
+ margin: 10px 0;
+ width: 250px;
+ color: $color-green-light;
+ transform: translateX(-300px);
+ transition: all 1s ease;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ .notification-title {
+ font-size: 0.9em;
+ margin: 0;
+ }
+
+ .notification-description {
+ font-size: 0.8em;
+ }
+
+ &.notification-open {
+ transform: translateX(0px);
+ }
+
+ &.notification-closed {
+ transform: translateY(-50px);
+ opacity: 0;
+ }
+
+ }
+
+
+} \ No newline at end of file
diff --git a/frontend/src/components/panels/PcapPane.js b/frontend/src/components/panels/PcapPane.js
index 31d8815..13f7cb3 100644
--- a/frontend/src/components/panels/PcapPane.js
+++ b/frontend/src/components/panels/PcapPane.js
@@ -9,28 +9,31 @@ 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 {
- constructor(props) {
- super(props);
-
- this.state = {
- sessions: [],
- isUploadFileValid: true,
- isUploadFileFocused: false,
- uploadFlushAll: false,
- isFileValid: true,
- isFileFocused: false,
- fileValue: "",
- processFlushAll: false,
- deleteOriginalFile: false
- };
- }
+ state = {
+ sessions: [],
+ isUploadFileValid: true,
+ isUploadFileFocused: false,
+ uploadFlushAll: false,
+ isFileValid: true,
+ isFileFocused: false,
+ fileValue: "",
+ processFlushAll: false,
+ deleteOriginalFile: false
+ };
componentDidMount() {
this.loadSessions();
+ dispatcher.register("notifications", payload => {
+ if (payload.event === "pcap.upload" || payload.event === "pcap.file") {
+ this.loadSessions();
+ }
+ });
+
document.title = "caronte:~/pcaps$";
}
diff --git a/frontend/src/components/panels/RulePane.js b/frontend/src/components/panels/RulePane.js
index 4641378..76f3ac0 100644
--- a/frontend/src/components/panels/RulePane.js
+++ b/frontend/src/components/panels/RulePane.js
@@ -14,35 +14,13 @@ 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 {
- constructor(props) {
- super(props);
-
- this.state = {
- rules: [],
- newRule: this.emptyRule,
- newPattern: this.emptyPattern
- };
-
- this.directions = {
- 0: "both",
- 1: "c->s",
- 2: "s->c"
- };
- }
-
- componentDidMount() {
- this.reset();
- this.loadRules();
-
- document.title = "caronte:~/rules$";
- }
-
emptyRule = {
"name": "",
"color": "",
@@ -60,7 +38,6 @@ class RulePane extends Component {
},
"version": 0
};
-
emptyPattern = {
"regex": "",
"flags": {
@@ -74,6 +51,34 @@ class RulePane extends Component {
"max_occurrences": 0,
"direction": 0
};
+ state = {
+ rules: [],
+ newRule: this.emptyRule,
+ newPattern: this.emptyPattern
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.directions = {
+ 0: "both",
+ 1: "c->s",
+ 2: "s->c"
+ };
+ }
+
+ componentDidMount() {
+ this.reset();
+ this.loadRules();
+
+ dispatcher.register("notifications", 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}))
@@ -226,17 +231,17 @@ class RulePane extends Component {
<tr key={r.id} onClick={() => {
this.reset();
this.setState({selectedRule: _.cloneDeep(r)});
- }} className={classNames("row-small", "row-clickable", {"row-selected": rule.id === r.id })}>
+ }} className={classNames("row-small", "row-clickable", {"row-selected": rule.id === r.id})}>
<td>{r["id"].substring(0, 8)}</td>
<td>{r["name"]}</td>
- <td><ButtonField name={r["color"]} color={r["color"]} small /></td>
+ <td><ButtonField name={r["color"]} color={r["color"]} small/></td>
<td>{r["notes"]}</td>
</tr>
);
let patterns = (this.state.selectedPattern == null && !isUpdate ?
- rule.patterns.concat(this.state.newPattern) :
- rule.patterns
+ rule.patterns.concat(this.state.newPattern) :
+ rule.patterns
).map(p => p === pattern ?
<tr key={randomClassName()}>
<td style={{"width": "500px"}}>
@@ -244,7 +249,7 @@ class RulePane extends Component {
onChange={(v) => {
this.updateParam(() => pattern.regex = v);
this.setState({patternRegexFocused: pattern.regex === ""});
- }} />
+ }}/>
</td>
<td><CheckField small checked={pattern.flags.caseless}
onChange={(v) => this.updateParam(() => pattern.flags.caseless = v)}/></td>
@@ -259,34 +264,35 @@ class RulePane extends Component {
<td style={{"width": "70px"}}>
<NumericField small value={pattern.min_occurrences}
active={this.state.patternOccurrencesFocused}
- onChange={(v) => this.updateParam(() => pattern.min_occurrences = v)} />
+ onChange={(v) => this.updateParam(() => pattern.min_occurrences = v)}/>
</td>
<td style={{"width": "70px"}}>
<NumericField small value={pattern.max_occurrences}
active={this.state.patternOccurrencesFocused}
- onChange={(v) => this.updateParam(() => pattern.max_occurrences = v)} />
+ onChange={(v) => this.updateParam(() => pattern.max_occurrences = v)}/>
</td>
<td><ChoiceField inline small keys={[0, 1, 2]} values={["both", "c->s", "s->c"]}
value={this.directions[pattern.direction]}
- onChange={(v) => this.updateParam(() => pattern.direction = v)} /></td>
+ onChange={(v) => this.updateParam(() => pattern.direction = v)}/></td>
<td>{this.state.selectedPattern == null ?
<ButtonField variant="green" small name="add" inline rounded onClick={() => this.addPattern(p)}/> :
- <ButtonField variant="green" small name="save" inline rounded onClick={() => this.updatePattern(p)}/>}
+ <ButtonField variant="green" small name="save" inline rounded
+ onClick={() => this.updatePattern(p)}/>}
</td>
</tr>
:
<tr key={"new_pattern"} className="row-small">
<td>{p.regex}</td>
- <td>{p.flags.caseless ? "yes": "no"}</td>
- <td>{p.flags.dot_all ? "yes": "no"}</td>
- <td>{p.flags.multi_line ? "yes": "no"}</td>
- <td>{p.flags.utf_8_mode ? "yes": "no"}</td>
- <td>{p.flags.unicode_property ? "yes": "no"}</td>
+ <td>{p.flags.caseless ? "yes" : "no"}</td>
+ <td>{p.flags.dot_all ? "yes" : "no"}</td>
+ <td>{p.flags.multi_line ? "yes" : "no"}</td>
+ <td>{p.flags.utf_8_mode ? "yes" : "no"}</td>
+ <td>{p.flags.unicode_property ? "yes" : "no"}</td>
<td>{p.min_occurrences}</td>
<td>{p.max_occurrences}</td>
<td>{this.directions[p.direction]}</td>
{!isUpdate && <td><ButtonField variant="blue" small rounded name="edit"
- onClick={() => this.editPattern(p) }/></td>}
+ onClick={() => this.editPattern(p)}/></td>}
</tr>
);
@@ -296,9 +302,9 @@ class RulePane extends Component {
<div className="section-header">
<span className="api-request">GET /api/rules</span>
{this.state.rulesStatusCode &&
- <span className="api-response"><LinkPopover text={this.state.rulesStatusCode}
- content={this.state.rulesResponse}
- placement="left" /></span>}
+ <span className="api-response"><LinkPopover text={this.state.rulesStatusCode}
+ content={this.state.rulesResponse}
+ placement="left"/></span>}
</div>
<div className="section-content">
@@ -327,7 +333,7 @@ class RulePane extends Component {
</span>
<span className="api-response"><LinkPopover text={this.state.ruleStatusCode}
content={this.state.ruleResponse}
- placement="left" /></span>
+ placement="left"/></span>
</div>
<div className="section-content">
@@ -336,11 +342,11 @@ class RulePane extends Component {
<Col>
<InputField name="name" inline value={rule.name}
onChange={(v) => this.updateParam((r) => r.name = v)}
- error={this.state.ruleNameError} />
+ error={this.state.ruleNameError}/>
<ColorField inline value={rule.color} error={this.state.ruleColorError}
- onChange={(v) => this.updateParam((r) => r.color = v)} />
+ onChange={(v) => this.updateParam((r) => r.color = v)}/>
<TextField name="notes" rows={2} value={rule.notes}
- onChange={(v) => this.updateParam((r) => r.notes = v)} />
+ onChange={(v) => this.updateParam((r) => r.notes = v)}/>
</Col>
<Col style={{"paddingTop": "6px"}}>
@@ -348,29 +354,29 @@ class RulePane extends Component {
<NumericField name="service_port" inline value={rule.filter.service_port}
onChange={(v) => this.updateParam((r) => r.filter.service_port = v)}
min={0} max={65565} error={this.state.ruleServicePortError}
- readonly={isUpdate} />
+ readonly={isUpdate}/>
<NumericField name="client_port" inline value={rule.filter.client_port}
onChange={(v) => this.updateParam((r) => r.filter.client_port = v)}
min={0} max={65565} error={this.state.ruleClientPortError}
- readonly={isUpdate} />
+ readonly={isUpdate}/>
<InputField name="client_address" value={rule.filter.client_address}
error={this.state.ruleClientAddressError} readonly={isUpdate}
- onChange={(v) => this.updateParam((r) => r.filter.client_address = v)} />
+ onChange={(v) => this.updateParam((r) => r.filter.client_address = v)}/>
</Col>
<Col style={{"paddingTop": "11px"}}>
<NumericField name="min_duration" inline value={rule.filter.min_duration}
error={this.state.ruleDurationError} readonly={isUpdate}
- onChange={(v) => this.updateParam((r) => r.filter.min_duration = v)} />
+ onChange={(v) => this.updateParam((r) => r.filter.min_duration = v)}/>
<NumericField name="max_duration" inline value={rule.filter.max_duration}
error={this.state.ruleDurationError} readonly={isUpdate}
- onChange={(v) => this.updateParam((r) => r.filter.max_duration = v)} />
+ onChange={(v) => this.updateParam((r) => r.filter.max_duration = v)}/>
<NumericField name="min_bytes" inline value={rule.filter.min_bytes}
error={this.state.ruleBytesError} readonly={isUpdate}
- onChange={(v) => this.updateParam((r) => r.filter.min_bytes = v)} />
+ onChange={(v) => this.updateParam((r) => r.filter.min_bytes = v)}/>
<NumericField name="max_bytes" inline value={rule.filter.max_bytes}
error={this.state.ruleBytesError} readonly={isUpdate}
- onChange={(v) => this.updateParam((r) => r.filter.max_bytes = v)} />
+ onChange={(v) => this.updateParam((r) => r.filter.max_bytes = v)}/>
</Col>
</Row>
</Container>
@@ -388,7 +394,7 @@ class RulePane extends Component {
<th>min</th>
<th>max</th>
<th>direction</th>
- {!isUpdate && <th>actions</th> }
+ {!isUpdate && <th>actions</th>}
</tr>
</thead>
<tbody>
@@ -403,7 +409,7 @@ class RulePane extends Component {
<div className="section-footer">
{<ButtonField variant="red" name="cancel" bordered onClick={this.reset}/>}
<ButtonField variant={isUpdate ? "blue" : "green"} name={isUpdate ? "update_rule" : "add_rule"}
- bordered onClick={isUpdate ? this.updateRule : this.addRule} />
+ bordered onClick={isUpdate ? this.updateRule : this.addRule}/>
</div>
</div>
</div>
diff --git a/frontend/src/components/panels/ServicePane.js b/frontend/src/components/panels/ServicePane.js
index 0e99652..22c6655 100644
--- a/frontend/src/components/panels/ServicePane.js
+++ b/frontend/src/components/panels/ServicePane.js
@@ -12,28 +12,13 @@ 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 {
- constructor(props) {
- super(props);
-
- this.state = {
- services: [],
- currentService: this.emptyService,
- };
-
- document.title = "caronte:~/services$";
- }
-
- componentDidMount() {
- this.reset();
- this.loadServices();
- }
-
emptyService = {
"port": 0,
"name": "",
@@ -41,6 +26,24 @@ class ServicePane extends Component {
"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}))
diff --git a/frontend/src/components/panels/common.scss b/frontend/src/components/panels/common.scss
index 121a917..1468f35 100644
--- a/frontend/src/components/panels/common.scss
+++ b/frontend/src/components/panels/common.scss
@@ -2,11 +2,9 @@
.pane-container {
height: 100%;
- padding: 10px 10px 0;
background-color: $color-primary-3;
.pane-section {
- margin-bottom: 10px;
background-color: $color-primary-0;
.section-header {
@@ -14,7 +12,7 @@
font-weight: 500;
display: flex;
padding: 5px 10px;
- background-color: $color-primary-2;
+ background-color: $color-primary-3;
.api-request {
flex: 1;
diff --git a/frontend/src/dispatcher.js b/frontend/src/dispatcher.js
new file mode 100644
index 0000000..4b8b5a4
--- /dev/null
+++ b/frontend/src/dispatcher.js
@@ -0,0 +1,35 @@
+
+class Dispatcher {
+
+ constructor() {
+ this.listeners = [];
+ }
+
+ dispatch = (topic, payload) => {
+ this.listeners.filter(l => l.topic === topic).forEach(l => l.callback(payload));
+ };
+
+ register = (topic, callback) => {
+ if (typeof callback !== "function") {
+ throw new Error("dispatcher callback must be a function");
+ }
+ if (typeof topic === "string") {
+ this.listeners.push({topic, callback});
+ } else if (typeof topic === "object" && Array.isArray(topic)) {
+ topic.forEach(e => {
+ if (typeof e !== "string") {
+ throw new Error("all topics must be strings");
+ }
+ });
+
+ topic.forEach(e => this.listeners.push({e, callback}));
+ } else {
+ throw new Error("topic must be a string or an array of strings");
+ }
+ };
+
+}
+
+const dispatcher = new Dispatcher();
+
+export default dispatcher;
diff --git a/frontend/src/globals.js b/frontend/src/globals.js
deleted file mode 100644
index cd4dc64..0000000
--- a/frontend/src/globals.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import {Dispatcher} from "flux";
-
-const dispatcher = new Dispatcher();
-
-export default dispatcher;
diff --git a/frontend/src/index.js b/frontend/src/index.js
index 2e90371..beb52ae 100644
--- a/frontend/src/index.js
+++ b/frontend/src/index.js
@@ -4,6 +4,9 @@ import 'bootstrap/dist/css/bootstrap.css';
import './index.scss';
import App from './views/App';
import * as serviceWorker from './serviceWorker';
+import notifications from "./notifications";
+
+notifications.createWebsocket();
ReactDOM.render(
<React.StrictMode>
@@ -12,7 +15,4 @@ ReactDOM.render(
document.getElementById('root')
);
-// If you want your app to work offline and load faster, you can change
-// unregister() to register() below. Note this comes with some pitfalls.
-// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
diff --git a/frontend/src/notifications.js b/frontend/src/notifications.js
new file mode 100644
index 0000000..2a77ffb
--- /dev/null
+++ b/frontend/src/notifications.js
@@ -0,0 +1,40 @@
+import log from "./log";
+import dispatcher from "./dispatcher";
+
+class Notifications {
+
+ constructor() {
+ const location = document.location;
+ this.wsUrl = `ws://${location.hostname}${location.port ? ":" + location.port : ""}/ws`;
+ }
+
+ createWebsocket = () => {
+ this.ws = new WebSocket(this.wsUrl);
+ this.ws.onopen = this.onWebsocketOpen;
+ this.ws.onerror = this.onWebsocketError;
+ this.ws.onclose = this.onWebsocketClose;
+ this.ws.onmessage = this.onWebsocketMessage;
+ };
+
+ onWebsocketOpen = () => {
+ log.debug("Connected to backend with websocket");
+ };
+
+ onWebsocketError = (err) => {
+ this.ws.close();
+ log.error("Websocket error", err);
+ setTimeout(() => this.createWebsocket(), 3000);
+ };
+
+ onWebsocketClose = () => {
+ log.debug("Closed websocket connection with backend");
+ };
+
+ onWebsocketMessage = (message) => {
+ dispatcher.dispatch("notifications", JSON.parse(message.data));
+ };
+}
+
+const notifications = new Notifications();
+
+export default notifications;
diff --git a/frontend/src/setupProxy.js b/frontend/src/setupProxy.js
new file mode 100644
index 0000000..6f082c8
--- /dev/null
+++ b/frontend/src/setupProxy.js
@@ -0,0 +1,7 @@
+const { createProxyMiddleware } = require('http-proxy-middleware');
+
+module.exports = function(app) {
+ app.use(createProxyMiddleware("/api", { target: "http://localhost:3333" }));
+ app.use(createProxyMiddleware("/setup", { target: "http://localhost:3333" }));
+ app.use(createProxyMiddleware("/ws", { target: "http://localhost:3333", ws: true }));
+};
diff --git a/frontend/src/views/App.js b/frontend/src/views/App.js
index 00d9110..c14b7f5 100644
--- a/frontend/src/views/App.js
+++ b/frontend/src/views/App.js
@@ -5,18 +5,22 @@ import MainPane from "../components/panels/MainPane";
import Footer from "./Footer";
import {BrowserRouter as Router} from "react-router-dom";
import Filters from "./Filters";
-import backend from "../backend";
import ConfigurationPane from "../components/panels/ConfigurationPane";
-import log from "../log";
+import Notifications from "../components/Notifications";
+import dispatcher from "../dispatcher";
class App extends Component {
state = {};
componentDidMount() {
- backend.get("/api/services").then(_ => {
- log.debug("Caronte is already configured. Loading main..");
- this.setState({configured: true});
+ dispatcher.register("notifications", payload => {
+ if (payload.event === "connected") {
+ this.setState({
+ connected: true,
+ configured: payload.message["is_configured"]
+ });
+ }
});
setInterval(() => {
@@ -36,19 +40,22 @@ class App extends Component {
return (
<div className="main">
- <Router>
- <div className="main-header">
- <Header onOpenFilters={() => this.setState({filterWindowOpen: true})}/>
- </div>
- <div className="main-content">
- {this.state.configured ? <MainPane/> :
- <ConfigurationPane onConfigured={() => this.setState({configured: true})}/>}
- {modal}
- </div>
- <div className="main-footer">
- {this.state.configured && <Footer/>}
- </div>
- </Router>
+ <Notifications/>
+ {this.state.connected &&
+ <Router>
+ <div className="main-header">
+ <Header onOpenFilters={() => this.setState({filterWindowOpen: true})}/>
+ </div>
+ <div className="main-content">
+ {this.state.configured ? <MainPane/> :
+ <ConfigurationPane onConfigured={() => this.setState({configured: true})}/>}
+ {modal}
+ </div>
+ <div className="main-footer">
+ {this.state.configured && <Footer/>}
+ </div>
+ </Router>
+ }
</div>
);
}
diff --git a/frontend/src/views/Connections.js b/frontend/src/views/Connections.js
index fe655b3..bd631a2 100644
--- a/frontend/src/views/Connections.js
+++ b/frontend/src/views/Connections.js
@@ -6,9 +6,9 @@ import {Redirect} from 'react-router';
import {withRouter} from "react-router-dom";
import backend from "../backend";
import ConnectionMatchedRules from "../components/ConnectionMatchedRules";
-import dispatcher from "../globals";
import log from "../log";
import ButtonField from "../components/fields/ButtonField";
+import dispatcher from "../dispatcher";
class Connections extends Component {
@@ -17,8 +17,6 @@ class Connections extends Component {
connections: [],
firstConnection: null,
lastConnection: null,
- flagRule: null,
- rules: null,
queryString: null
};
@@ -41,14 +39,24 @@ class Connections extends Component {
// TODO: scroll to initial connection
}
- dispatcher.register((payload) => {
- if (payload.actionType === "timeline-update") {
- 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("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"));
}
});
}
@@ -116,6 +124,13 @@ class Connections extends Component {
}
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;
@@ -154,28 +169,29 @@ class Connections extends Component {
}
}
- let rules = this.state.rules;
- if (rules == null) {
- rules = (await backend.get("/api/rules")).json;
- }
-
this.setState({
loading: false,
connections: connections,
- rules: rules,
firstConnection: firstConnection,
lastConnection: lastConnection
});
if (firstConnection != null && lastConnection != null) {
- dispatcher.dispatch({
- actionType: "connections-update",
+ 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;
let queryString = this.state.queryString !== null ? this.state.queryString : "";
@@ -222,7 +238,8 @@ class Connections extends Component {
selected={this.state.selected === c.id}
onMarked={marked => c.marked = marked}
onEnabled={enabled => c.hidden = !enabled}
- addServicePortFilter={this.addServicePortFilter}/>,
+ addServicePortFilter={this.addServicePortFilter}
+ services={this.state.services}/>,
c.matched_rules.length > 0 &&
<ConnectionMatchedRules key={c.id + "_m"} matchedRules={c.matched_rules}
rules={this.state.rules}
diff --git a/frontend/src/views/Connections.scss b/frontend/src/views/Connections.scss
index 9e2b6ba..de06096 100644
--- a/frontend/src/views/Connections.scss
+++ b/frontend/src/views/Connections.scss
@@ -3,7 +3,6 @@
.connections-container {
position: relative;
height: 100%;
- padding: 10px;
background-color: $color-primary-3;
.connections {
@@ -21,7 +20,7 @@
top: 0;
padding: 5px;
border: none;
- background-color: $color-primary-2;
+ background-color: $color-primary-3;
}
&:hover::-webkit-scrollbar-thumb {
diff --git a/frontend/src/views/Footer.js b/frontend/src/views/Footer.js
index ad1e4f7..dcf9cf8 100644
--- a/frontend/src/views/Footer.js
+++ b/frontend/src/views/Footer.js
@@ -13,9 +13,9 @@ import {
import {TimeRange, TimeSeries} from "pondjs";
import backend from "../backend";
import ChoiceField from "../components/fields/ChoiceField";
-import dispatcher from "../globals";
import {withRouter} from "react-router-dom";
import log from "../log";
+import dispatcher from "../dispatcher";
class Footer extends Component {
@@ -39,15 +39,12 @@ class Footer extends Component {
componentDidMount() {
const filteredPort = this.filteredPort();
this.setState({filteredPort});
- this.loadStatistics(this.state.metric, filteredPort).then(() =>
- log.debug("Statistics loaded after mount"));
-
- dispatcher.register((payload) => {
- if (payload.actionType === "connections-update") {
- this.setState({
- selection: new TimeRange(payload.from, payload.to),
- });
- }
+ 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),
+ });
});
}
@@ -109,8 +106,7 @@ class Footer extends Component {
clearTimeout(this.selectionTimeout);
}
this.selectionTimeout = setTimeout(() => {
- dispatcher.dispatch({
- actionType: "timeline-update",
+ dispatcher.dispatch("timeline_updates", {
from: timeRange.begin(),
to: timeRange.end()
});
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index fa150ab..e3cade9 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -1656,6 +1656,13 @@
"@types/minimatch" "*"
"@types/node" "*"
+"@types/http-proxy@^1.17.4":
+ version "1.17.4"
+ resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.4.tgz#e7c92e3dbe3e13aa799440ff42e6d3a17a9d045b"
+ integrity sha512-IrSHl2u6AWXduUaDLqYpt45tLVCtYv7o4Z0s1KghBCDgIIS9oW5K1H8mZG/A2CfeLdEa7rTd1ACOiHBc1EMT2Q==
+ dependencies:
+ "@types/node" "*"
+
"@types/invariant@^2.2.33":
version "2.2.34"
resolved "https://registry.yarnpkg.com/@types/invariant/-/invariant-2.2.34.tgz#05e4f79f465c2007884374d4795452f995720bbe"
@@ -2707,7 +2714,7 @@ braces@^2.3.1, braces@^2.3.2:
split-string "^3.0.2"
to-regex "^3.0.1"
-braces@~3.0.2:
+braces@^3.0.1, braces@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
@@ -5738,7 +5745,18 @@ http-proxy-middleware@0.19.1:
lodash "^4.17.11"
micromatch "^3.1.10"
-http-proxy@^1.17.0:
+http-proxy-middleware@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-1.0.5.tgz#4c6e25d95a411e3d750bc79ccf66290675176dc2"
+ integrity sha512-CKzML7u4RdGob8wuKI//H8Ein6wNTEQR7yjVEzPbhBLGdOfkfvgTnp2HLnniKBDP9QW4eG10/724iTWLBeER3g==
+ dependencies:
+ "@types/http-proxy" "^1.17.4"
+ http-proxy "^1.18.1"
+ is-glob "^4.0.1"
+ lodash "^4.17.19"
+ micromatch "^4.0.2"
+
+http-proxy@^1.17.0, http-proxy@^1.18.1:
version "1.18.1"
resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549"
integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==
@@ -7434,6 +7452,14 @@ micromatch@^3.1.10, micromatch@^3.1.4:
snapdragon "^0.8.1"
to-regex "^3.0.2"
+micromatch@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259"
+ integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==
+ dependencies:
+ braces "^3.0.1"
+ picomatch "^2.0.5"
+
miller-rabin@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d"
@@ -8405,7 +8431,7 @@ performance-now@^2.1.0:
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
-picomatch@^2.0.4, picomatch@^2.2.1:
+picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1:
version "2.2.2"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==
diff --git a/go.mod b/go.mod
index 308b16b..404f64c 100644
--- a/go.mod
+++ b/go.mod
@@ -9,6 +9,7 @@ require (
github.com/go-playground/validator/v10 v10.2.0
github.com/golang/protobuf v1.3.5 // indirect
github.com/google/gopacket v1.1.17
+ github.com/gorilla/websocket v1.4.2
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/sirupsen/logrus v1.4.2
diff --git a/go.sum b/go.sum
index fd63c39..d29e0cb 100644
--- a/go.sum
+++ b/go.sum
@@ -58,6 +58,8 @@ github.com/google/gopacket v1.1.17 h1:rMrlX2ZY2UbvT+sdz3+6J+pp2z+msCq9MxTU6ymxbB
github.com/google/gopacket v1.1.17/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM=
github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c h1:16eHWuMGvCjSfgRJKqIzapE78onvvTbdi1rMkU00lZw=
github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
+github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
diff --git a/notification_controller.go b/notification_controller.go
new file mode 100644
index 0000000..88c9e8c
--- /dev/null
+++ b/notification_controller.go
@@ -0,0 +1,165 @@
+package main
+
+import (
+ "github.com/gin-gonic/gin"
+ "github.com/gorilla/websocket"
+ log "github.com/sirupsen/logrus"
+ "net"
+ "net/http"
+ "time"
+)
+
+const (
+ InsertNotification = "insert"
+ UpdateNotification = "update"
+ DeleteNotification = "delete"
+
+ writeWait = 10 * time.Second
+ pongWait = 60 * time.Second
+ pingPeriod = (pongWait * 9) / 10
+ maxMessageSize = 512
+)
+
+type NotificationController struct {
+ upgrader websocket.Upgrader
+ clients map[net.Addr]*client
+ broadcast chan interface{}
+ register chan *client
+ unregister chan *client
+ applicationContext *ApplicationContext
+}
+
+func NewNotificationController(applicationContext *ApplicationContext) *NotificationController {
+ return &NotificationController{
+ upgrader: websocket.Upgrader{
+ ReadBufferSize: 1024,
+ WriteBufferSize: 1024,
+ },
+ clients: make(map[net.Addr]*client),
+ broadcast: make(chan interface{}),
+ register: make(chan *client),
+ unregister: make(chan *client),
+ applicationContext: applicationContext,
+ }
+}
+
+type client struct {
+ conn *websocket.Conn
+ send chan interface{}
+ notificationController *NotificationController
+}
+
+func (wc *NotificationController) NotificationHandler(w http.ResponseWriter, r *http.Request) error {
+ conn, err := wc.upgrader.Upgrade(w, r, nil)
+ if err != nil {
+ log.WithError(err).Error("failed to set websocket upgrade")
+ return err
+ }
+
+ client := &client{
+ conn: conn,
+ send: make(chan interface{}),
+ notificationController: wc,
+ }
+ wc.register <- client
+ go client.readPump()
+ go client.writePump()
+
+ return nil
+}
+
+func (wc *NotificationController) Run() {
+ for {
+ select {
+ case client := <-wc.register:
+ wc.clients[client.conn.RemoteAddr()] = client
+ payload := gin.H{"event": "connected", "message": gin.H{
+ "version": wc.applicationContext.Version,
+ "is_configured": wc.applicationContext.IsConfigured,
+ "connected_clients": len(wc.clients),
+ }}
+ client.send <- payload
+ log.WithField("connected_clients", len(wc.clients)).
+ WithField("remote_address", client.conn.RemoteAddr()).
+ Info("[+] a websocket client connected")
+ case client := <-wc.unregister:
+ if _, ok := wc.clients[client.conn.RemoteAddr()]; ok {
+ close(client.send)
+ _ = client.conn.WriteMessage(websocket.CloseMessage, nil)
+ _ = client.conn.Close()
+ delete(wc.clients, client.conn.RemoteAddr())
+ log.WithField("connected_clients", len(wc.clients)).
+ WithField("remote_address", client.conn.RemoteAddr()).
+ Info("[-] a websocket client disconnected")
+ }
+ case payload := <-wc.broadcast:
+ for _, client := range wc.clients {
+ select {
+ case client.send <- payload:
+ default:
+ close(client.send)
+ delete(wc.clients, client.conn.RemoteAddr())
+ }
+ }
+ }
+ }
+}
+
+func (wc *NotificationController) Notify(event string, eventType string, message interface{}) {
+ wc.broadcast <- gin.H{"event": event, "event_type": eventType, "message": message}
+}
+
+func (c *client) readPump() {
+ c.conn.SetReadLimit(maxMessageSize)
+ if err := c.conn.SetReadDeadline(time.Now().Add(pongWait)); err != nil {
+ c.close()
+ return
+ }
+ c.conn.SetPongHandler(func(string) error { return c.conn.SetReadDeadline(time.Now().Add(pongWait)) })
+ for {
+ if _, _, err := c.conn.ReadMessage(); err != nil {
+ if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
+ log.WithError(err).WithField("remote_address", c.conn.RemoteAddr()).
+ Warn("unexpected websocket disconnection")
+ }
+ break
+ }
+ }
+
+ c.close()
+}
+
+func (c *client) writePump() {
+ ticker := time.NewTicker(pingPeriod)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case payload, ok := <-c.send:
+ if !ok {
+ return
+ }
+ if err := c.conn.SetWriteDeadline(time.Now().Add(writeWait)); err != nil {
+ c.close()
+ return
+ }
+ if err := c.conn.WriteJSON(payload); err != nil {
+ c.close()
+ return
+ }
+ case <-ticker.C:
+ if err := c.conn.SetWriteDeadline(time.Now().Add(writeWait)); err != nil {
+ c.close()
+ return
+ }
+ if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
+ c.close()
+ return
+ }
+ }
+ }
+}
+
+func (c *client) close() {
+ c.notificationController.unregister <- c
+}
diff --git a/pcap_importer.go b/pcap_importer.go
index 1739b3f..78a5e6c 100644
--- a/pcap_importer.go
+++ b/pcap_importer.go
@@ -19,7 +19,6 @@ import (
const PcapsBasePath = "pcaps/"
const ProcessingPcapsBasePath = PcapsBasePath + "processing/"
const initialAssemblerPoolSize = 16
-const flushOlderThan = 5 * time.Minute
const importUpdateProgressInterval = 100 * time.Millisecond
type PcapImporter struct {
@@ -201,8 +200,8 @@ func (pi *PcapImporter) parsePcap(session ImportingSession, fileName string, flu
var servicePort uint16
var index int
- isDstServer := pi.serverNet.Contains(packet.NetworkLayer().NetworkFlow().Dst().Raw())
- isSrcServer := pi.serverNet.Contains(packet.NetworkLayer().NetworkFlow().Src().Raw())
+ isDstServer := pi.serverNet.Contains(packet.NetworkLayer().NetworkFlow().Dst().Raw())
+ isSrcServer := pi.serverNet.Contains(packet.NetworkLayer().NetworkFlow().Src().Raw())
if isDstServer && !isSrcServer {
servicePort = uint16(tcp.DstPort)
index = 0
@@ -284,7 +283,7 @@ func deleteProcessingFile(fileName string) {
}
func moveProcessingFile(sessionID string, fileName string) {
- if err := os.Rename(ProcessingPcapsBasePath + fileName, PcapsBasePath + sessionID + path.Ext(fileName)); err != nil {
+ if err := os.Rename(ProcessingPcapsBasePath+fileName, PcapsBasePath+sessionID+path.Ext(fileName)); err != nil {
log.WithError(err).Error("failed to move processed file")
}
}