diff options
30 files changed, 624 insertions, 183 deletions
@@ -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, @@ -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== @@ -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 @@ -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") } } |