Compare commits

..

15 Commits

Author SHA1 Message Date
gadotroee
6bab381280 Make kratos image configurable (#670) 2022-01-20 13:48:02 +02:00
gadotroee
27dee4e09b TRA-4193 - Try port forward if proxy is not available (#662) 2022-01-20 11:33:00 +02:00
M. Mert Yıldıran
b31af7214b TRA-4140 Fix HTTP/1.0 is recognized as HTTP/1.1 (#666) 2022-01-20 11:02:21 +03:00
Gustavo Massaneiro
d5fd2ff1da Service Map (#623)
* debug builds and gcflags

* update dockerfile for debug

* service map routes and controller

* service map graph structure

* service map interface and new methods

* adding service map edges from mizu entries

* new service map count methods

* implementing the status endpoint

* ServiceMapResponse and ServiceMapEdge models

* service map get endpoint logic

* reset logic and endpoint

* fixed service map get status

* improvements to graph node structure

* front-end implementation and service map buttons

* new render endpoint to render the graph in real time

* spinner sass

* new ServiceMapModal component

* testing react-force-graph-2d lib

* Improvements to service map graph structure, added node id and updated edge source/destination type

* Revert "testing react-force-graph-2d lib"

This reverts commit 1153938386.

* testing react-graph-vis lib

* updated to work with react-graph-vis lib

* removed render endpoint

* go mod tidy

* serviceMap config flag

* using the serviceMap config flag

* passing mizu config to service map as a dependency

* service map tests

* Removed print functions

* finished service map tests

* new service property

* service map controller tests

* moved service map reset button to service map modal
reset closes the modal

* service map modal refresh button and logic

* reset button resets data and refresh

* service map modal close button

* node size/edge size based on the count value
edge color based on protocol

* nodes and edges shadow

* enabled physics to avoid node overlap, changed kafka protocol color to dark green

* showing edges count values and fixed bidirectional edges overlap

* go mod tidy

* removed console.log

* Using the destination node protocol instead of the source node protocol

* Revert "debug builds and gcflags"
Addressed by #624 and #626

This reverts commit 17ecaece3e.

* Revert "update dockerfile for debug"
Addressed by #635

This reverts commit 5dfc15b140.

* using the entire tap Protocol struct instead of only the protocol name

* using the backend protocol background color for node colors

* fixed test, the node list order can change

* re-factoring to get 100% coverage

* using protocol colors just for edges

* re-factored service map to use TCP Entry data. Node key is the entry ip-address instead of the name

* fallback to ip-address when entry name is unresolved

* re-factored front-end

* adjustment to main div style

* added support for multiple protocols for the same edge

* using the item protocol instead of the extension variable

* fixed controller tests

* displaying service name and ip-address on graph nodes

* fixed service map test, we cannot guarantee the slice order

* auth middleware

* created a new pkg for the service map

* re-factoring

* re-factored front-end

* reverting the import order as previous

* Aligning with other UI feature flags handling

* we don't need to get the status anymore, we have window["isServiceMapEnabled"]

* small adjustments

* renamed from .tsx to .ts

* button styles and minor improvements

* moved service map modal from trafficPage to app component

Co-authored-by: Igor Gov <igor.govorov1@gmail.com>
2022-01-19 15:27:12 -03:00
Igor Gov
7477f867f9 TRA-4122 - OAS dialog 2 (#637)
* parent d97d481392
author Amit Fainholts <amit@up9.com> 1641398982 +0200
committer Igor Gov <igor.govorov1@gmail.com> 1642070154 +0200

init

* revert

* small fixes

* api request to oas added

* redoc version downgraded and remove redundant package-lock

* remove redundant package json

* redoc updated to latest

* libraries updated to prevent vulnerabilities

* pr fixes

* remove ! from global var

* update useEffect in OasModal and other pr fixes

* change errors messages

Co-authored-by: Amit Fainholts <amit@up9.com>
Co-authored-by: Igor Gov <igor.govorov1@gmail.com>
Co-authored-by: lirazyehezkel <61656597+lirazyehezkel@users.noreply.github.com>
2022-01-19 13:06:39 +02:00
Adam Kol
cc4638afe6 Cypress: two new tests --> IgnoredUserAgents and RegexMasking (#663) 2022-01-18 17:32:50 +02:00
M. Mert Yıldıran
acf3894824 Fix EntryItem border on heading mode (#661) 2022-01-18 17:37:58 +03:00
leon-up9
6235217ead Bug/UI/tra 4169 apply qury by enter (#660)
* add shortcuts listeners and config

* key added

* listen for shortcut on input

* refactoring listensing to Enter Press

* comment for support

Co-authored-by: Leon <>
2022-01-18 12:08:53 +02:00
Adam Kol
c8a3033f87 Cypress: fix redact tests (#658) 2022-01-17 12:57:12 +02:00
Adam Kol
6b4bcc8abd Cypress: refactor for Redact and NoRedact tests (#656) 2022-01-17 10:43:39 +02:00
RoyUP9
5ca3107422 Added build ui to pr validation flow (#655) 2022-01-16 17:43:18 +02:00
lirazyehezkel
ce477095fd Mizu recoil fix (#654) 2022-01-16 15:44:08 +02:00
lirazyehezkel
5fed5808d2 TRA-4159 Mizu state management (#631)
* initiate recoil state management with entPage and tappingStatus

* first recoil selector

* insert entries and focusedEntryId into recoil

* ws connection, entry data

* manage query by recoil

* identifier for cypress

* conflicts fix

* css fix

* cr fixes

Co-authored-by: gadotroee <55343099+gadotroee@users.noreply.github.com>
2022-01-16 15:27:09 +02:00
Igor Gov
5c59cd643a Adding badges: latest release, license, slack (#653) 2022-01-16 14:58:18 +02:00
Adam Kol
aae03c52e9 UI important identifier (#652) 2022-01-16 14:17:18 +02:00
85 changed files with 3129 additions and 653 deletions

View File

@@ -44,3 +44,18 @@ jobs:
- name: Build Agent
run: make agent
build-ui:
name: Build UI
runs-on: ubuntu-latest
steps:
- name: Set up Node 14
uses: actions/setup-node@v2
with:
node-version: '14'
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Build UI
run: make ui

View File

@@ -1,5 +1,17 @@
![Mizu: The API Traffic Viewer for Kubernetes](assets/mizu-logo.svg)
<p align="center">
<a href="https://github.com/up9inc/mizu/releases/latest">
<img alt="GitHub Latest Release" src="https://img.shields.io/github/v/release/up9inc/mizu?logo=GitHub&style=flat-square">
</a>
<a href="https://github.com/up9inc/mizu/blob/main/LICENSE">
<img alt="GitHub License" src="https://img.shields.io/github/license/up9inc/mizu?logo=GitHub&style=flat-square">
</a>
<a href="https://join.slack.com/t/up9/shared_invite/zt-tfjnduli-QzlR8VV4Z1w3YnPIAJfhlQ">
<img alt="Slack" src="https://img.shields.io/badge/slack-join_chat-white.svg?logo=slack&style=social">
</a>
</p>
# The API Traffic Viewer for Kubernetes
A simple-yet-powerful API traffic viewer for Kubernetes enabling you to view all API communication between microservices to help your debug and troubleshoot regressions.
@@ -12,7 +24,7 @@ Think TCPDump and Wireshark re-invented for Kubernetes.
- Simple and powerful CLI
- Monitoring network traffic in real-time. Supported protocols:
- [HTTP/1.1](https://datatracker.ietf.org/doc/html/rfc2616) (REST, etc.)
- [HTTP/1.x](https://datatracker.ietf.org/doc/html/rfc2616) (REST, GraphQL, SOAP, etc.)
- [HTTP/2](https://datatracker.ietf.org/doc/html/rfc7540) (gRPC)
- [AMQP](https://www.rabbitmq.com/amqp-0-9-1-reference.html) (RabbitMQ, Apache Qpid, etc.)
- [Apache Kafka](https://kafka.apache.org/protocol)

View File

@@ -0,0 +1,22 @@
{
"watchForFileChanges":false,
"viewportWidth": 1920,
"viewportHeight": 1080,
"video": false,
"screenshotOnRunFailure": false,
"testFiles": [
"tests/GuiPort.js",
"tests/MultipleNamespaces.js",
"tests/Redact.js",
"tests/NoRedact.js",
"tests/Regex.js",
"tests/RegexMasking.js"
],
"env": {
"testUrl": "http://localhost:8899/",
"redactHeaderContent": "User-Header[REDACTED]",
"redactBodyContent": "{ \"User\": \"[REDACTED]\" }",
"regexMaskingBodyContent": "[REDACTED]"
}
}

View File

@@ -1,15 +1,12 @@
{
"watchForFileChanges":false,
"viewportWidth": 1920,
"viewportHeight": 1080,
"viewportHeight": 3500,
"video": false,
"screenshotOnRunFailure": false,
"testFiles":
["tests/GuiPort.js",
"tests/MultipleNamespaces.js",
"tests/RedactTests.js",
"tests/Regex.js"],
"testFiles": [
"tests/IgnoredUserAgents.js"
],
"env": {
"testUrl": "http://localhost:8899/"

View File

@@ -0,0 +1,9 @@
export function isValueExistsInElement(shouldInclude, content, domPathToContainer){
it(`should ${shouldInclude ? '' : 'not'} include '${content}'`, function () {
cy.get(domPathToContainer).then(htmlText => {
const allTextString = htmlText.text();
if (allTextString.includes(content) !== shouldInclude)
throw new Error(`One of the containers part contains ${content}`)
});
});
}

View File

@@ -0,0 +1,33 @@
import {isValueExistsInElement} from "../testHelpers/TrafficHelper";
it('Loading Mizu', function () {
cy.visit(Cypress.env('testUrl'));
});
it('going through each entry', function () {
cy.get('#total-entries').then(number => {
const getNum = () => {
const numOfEntries = number.text();
return parseInt(numOfEntries);
};
cy.wrap({ there: getNum }).invoke('there').should('be.gte', 25);
checkThatAllEntriesShown();
const entriesNum = getNum();
[...Array(entriesNum).keys()].map(checkEntry);
});
});
function checkThatAllEntriesShown() {
cy.get('#entries-length').then(number => {
if (number.text() === '1')
cy.get('[title="Fetch old records"]').click();
});
}
function checkEntry(entryIndex) {
cy.get(`#entry-${entryIndex}`).click();
cy.get('#tbody-Headers').should('be.visible');
isValueExistsInElement(false, 'Ignored-User-Agent', '#tbody-Headers');
}

View File

@@ -1,4 +1,4 @@
import {findLineAndCheck, getExpectedDetailsDict} from '../page_objects/StatusBar';
import {findLineAndCheck, getExpectedDetailsDict} from '../testHelpers/StatusBarHelper';
it('opening', function () {
cy.visit(Cypress.env('testUrl'));
@@ -15,4 +15,3 @@ function doItFunc(number) {
findLineAndCheck(getExpectedDetailsDict(podName, namespace));
});
}

View File

@@ -0,0 +1,8 @@
import {isValueExistsInElement} from '../testHelpers/TrafficHelper';
it('Loading Mizu', function () {
cy.visit(Cypress.env('testUrl'));
})
isValueExistsInElement(false, Cypress.env('redactHeaderContent'), '#tbody-Headers');
isValueExistsInElement(false, Cypress.env('redactBodyContent'), '.hljs');

View File

@@ -0,0 +1,8 @@
import {isValueExistsInElement} from '../testHelpers/TrafficHelper';
it('Loading Mizu', function () {
cy.visit(Cypress.env('testUrl'));
})
isValueExistsInElement(true, Cypress.env('redactHeaderContent'), '#tbody-Headers');
isValueExistsInElement(true, Cypress.env('redactBodyContent'), '.hljs');

View File

@@ -1,23 +0,0 @@
const inHeader = 'User-Header[REDACTED]';
const inBody = '{ "User": "[REDACTED]" }';
const shouldExist = Cypress.env('shouldExist');
it('Loading Mizu', function () {
cy.visit(Cypress.env('testUrl'));
})
it(`should ${shouldExist ? '' : 'not'} include ${inHeader}`, function () {
cy.get('.CollapsibleContainer', { timeout : 15 * 1000}).first().next().then(headerElements => { //TODO change the path and refactor the body and head functions
const allText = headerElements.text();
if (allText.includes(inHeader) !== shouldExist)
throw new Error(`The headers panel doesnt include ${inHeader}`);
});
});
it(`should ${shouldExist ? '' : 'not'} include ${inBody}`, function () {
cy.get('.hljs').then(bodyElement => {
const line = bodyElement.text();
if (line.includes(inBody) !== shouldExist)
throw new Error(`The body panel doesnt include ${inBody}`);
});
});

View File

@@ -1,4 +1,4 @@
import {getExpectedDetailsDict, checkLine} from '../page_objects/StatusBar';
import {getExpectedDetailsDict, checkLine} from '../testHelpers/StatusBarHelper';
it('opening', function () {

View File

@@ -0,0 +1,7 @@
import {isValueExistsInElement} from "../testHelpers/TrafficHelper";
it('Loading Mizu', function () {
cy.visit(Cypress.env('testUrl'));
})
isValueExistsInElement(true, Cypress.env('regexMaskingBodyContent'), '.hljs');

View File

@@ -138,7 +138,7 @@ func TestTapGuiPort(t *testing.T) {
return
}
runCypressTests(t, fmt.Sprintf("npx cypress run --spec \"cypress/integration/tests/GuiPort.js\" --env port=%d", guiPort))
runCypressTests(t, fmt.Sprintf("npx cypress run --spec \"cypress/integration/tests/GuiPort.js\" --env port=%d --config-file cypress/integration/configurations/Default.json", guiPort))
})
}
}
@@ -184,7 +184,7 @@ func TestTapAllNamespaces(t *testing.T) {
return
}
runCypressTests(t, fmt.Sprintf("npx cypress run --spec \"cypress/integration/tests/MultipleNamespaces.js\" --env name1=%v,name2=%v,name3=%v,namespace1=%v,namespace2=%v,namespace3=%v",
runCypressTests(t, fmt.Sprintf("npx cypress run --spec \"cypress/integration/tests/MultipleNamespaces.js\" --env name1=%v,name2=%v,name3=%v,namespace1=%v,namespace2=%v,namespace3=%v --config-file cypress/integration/configurations/Default.json",
expectedPods[0].Name, expectedPods[1].Name, expectedPods[2].Name, expectedPods[0].Namespace, expectedPods[1].Namespace, expectedPods[2].Namespace))
}
@@ -233,7 +233,7 @@ func TestTapMultipleNamespaces(t *testing.T) {
return
}
runCypressTests(t, fmt.Sprintf("npx cypress run --spec \"cypress/integration/tests/MultipleNamespaces.js\" --env name1=%v,name2=%v,name3=%v,namespace1=%v,namespace2=%v,namespace3=%v",
runCypressTests(t, fmt.Sprintf("npx cypress run --spec \"cypress/integration/tests/MultipleNamespaces.js\" --env name1=%v,name2=%v,name3=%v,namespace1=%v,namespace2=%v,namespace3=%v --config-file cypress/integration/configurations/Default.json",
expectedPods[0].Name, expectedPods[1].Name, expectedPods[2].Name, expectedPods[0].Namespace, expectedPods[1].Namespace, expectedPods[2].Namespace))
}
@@ -279,7 +279,7 @@ func TestTapRegex(t *testing.T) {
return
}
runCypressTests(t, fmt.Sprintf("npx cypress run --spec \"cypress/integration/tests/Regex.js\" --env name=%v,namespace=%v",
runCypressTests(t, fmt.Sprintf("npx cypress run --spec \"cypress/integration/tests/Regex.js\" --env name=%v,namespace=%v --config-file cypress/integration/configurations/Default.json",
expectedPods[0].Name, expectedPods[0].Namespace))
}
@@ -377,7 +377,7 @@ func TestTapRedact(t *testing.T) {
}
}
runCypressTests(t, fmt.Sprintf("npx cypress run --spec \"cypress/integration/tests/RedactTests.js\" --env shouldExist=true"))
runCypressTests(t, fmt.Sprintf("npx cypress run --spec \"cypress/integration/tests/Redact.js\" --config-file cypress/integration/configurations/Default.json"))
}
func TestTapNoRedact(t *testing.T) {
@@ -429,7 +429,7 @@ func TestTapNoRedact(t *testing.T) {
}
}
runCypressTests(t, "npx cypress run --spec \"cypress/integration/tests/RedactTests.js\" --env shouldExist=false")
runCypressTests(t, "npx cypress run --spec \"cypress/integration/tests/NoRedact.js\" --config-file cypress/integration/configurations/Default.json")
}
func TestTapRegexMasking(t *testing.T) {
@@ -480,41 +480,8 @@ func TestTapRegexMasking(t *testing.T) {
}
}
redactCheckFunc := func() error {
timestamp := time.Now().UnixNano() / int64(time.Millisecond)
runCypressTests(t, "npx cypress run --spec \"cypress/integration/tests/RegexMasking.js\" --config-file cypress/integration/configurations/Default.json")
entries, err := getDBEntries(timestamp, defaultEntriesCount, 1*time.Second)
if err != nil {
return err
}
err = checkEntriesAtLeast(entries, 1)
if err != nil {
return err
}
firstEntry := entries[0]
entryUrl := fmt.Sprintf("%v/entries/%v", apiServerUrl, firstEntry["id"])
requestResult, requestErr := executeHttpGetRequest(entryUrl)
if requestErr != nil {
return fmt.Errorf("failed to get entry, err: %v", requestErr)
}
entry := requestResult.(map[string]interface{})["data"].(map[string]interface{})
request := entry["request"].(map[string]interface{})
postData := request["postData"].(map[string]interface{})
textData := postData["text"].(string)
if textData != "[REDACTED]" {
return fmt.Errorf("unexpected result - body is not redacted")
}
return nil
}
if err := retriesExecute(shortRetriesCount, redactCheckFunc); err != nil {
t.Errorf("%v", err)
return
}
}
func TestTapIgnoredUserAgents(t *testing.T) {
@@ -575,45 +542,7 @@ func TestTapIgnoredUserAgents(t *testing.T) {
}
}
ignoredUserAgentsCheckFunc := func() error {
timestamp := time.Now().UnixNano() / int64(time.Millisecond)
entries, err := getDBEntries(timestamp, defaultEntriesCount, 1*time.Second)
if err != nil {
return err
}
err = checkEntriesAtLeast(entries, 1)
if err != nil {
return err
}
for _, entryInterface := range entries {
entryUrl := fmt.Sprintf("%v/entries/%v", apiServerUrl, entryInterface["id"])
requestResult, requestErr := executeHttpGetRequest(entryUrl)
if requestErr != nil {
return fmt.Errorf("failed to get entry, err: %v", requestErr)
}
entry := requestResult.(map[string]interface{})["data"].(map[string]interface{})
request := entry["request"].(map[string]interface{})
headers := request["_headers"].([]interface{})
for _, headerInterface := range headers {
header := headerInterface.(map[string]interface{})
if header["name"].(string) != ignoredUserAgentCustomHeader {
continue
}
return fmt.Errorf("unexpected result - user agent is not ignored")
}
}
return nil
}
if err := retriesExecute(shortRetriesCount, ignoredUserAgentsCheckFunc); err != nil {
t.Errorf("%v", err)
return
}
runCypressTests(t, "npx cypress run --spec \"cypress/integration/tests/IgnoredUserAgents.js\" --config-file cypress/integration/configurations/HugeMizu.json")
}
func TestTapDumpLogs(t *testing.T) {

View File

@@ -19,6 +19,7 @@ require (
github.com/orcaman/concurrent-map v0.0.0-20210106121528-16402b402231
github.com/ory/kratos-client-go v0.8.2-alpha.1
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/stretchr/testify v1.7.0
github.com/up9inc/basenine/client/go v0.0.0-20220110083745-04fbc6c2068d
github.com/up9inc/mizu/shared v0.0.0
github.com/up9inc/mizu/tap v0.0.0

View File

@@ -115,6 +115,7 @@ github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4Kfc
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc=
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
@@ -378,6 +379,7 @@ github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS4
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8=
github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=

View File

@@ -13,6 +13,7 @@ import (
"mizuserver/pkg/models"
"mizuserver/pkg/oas"
"mizuserver/pkg/routes"
"mizuserver/pkg/servicemap"
"mizuserver/pkg/up9"
"mizuserver/pkg/utils"
"net/http"
@@ -153,6 +154,9 @@ func enableExpFeatureIfNeeded() {
if config.Config.OAS {
oas.GetOasGeneratorInstance().Start()
}
if config.Config.ServiceMap {
servicemap.GetInstance().SetConfig(config.Config)
}
}
func configureBasenineServer(host string, port string) {
@@ -254,10 +258,12 @@ func hostApi(socketHarOutputChannel chan<- *tapApi.OutputChannelItem) {
routes.UserRoutes(app)
routes.InstallRoutes(app)
}
if config.Config.OAS {
routes.OASRoutes(app)
}
if config.Config.ServiceMap {
routes.ServiceMapRoutes(app)
}
routes.QueryRoutes(app)
routes.EntriesRoutes(app)
@@ -286,6 +292,7 @@ func setUIFlags() error {
replacedContent := strings.Replace(string(read), "__IS_STANDALONE__", strconv.FormatBool(config.Config.StandaloneMode), 1)
replacedContent = strings.Replace(replacedContent, "__IS_OAS_ENABLED__", strconv.FormatBool(config.Config.OAS), 1)
replacedContent = strings.Replace(replacedContent, "__IS_SERVICE_MAP_ENABLED__", strconv.FormatBool(config.Config.ServiceMap), 1)
err = ioutil.WriteFile(uiIndexPath, []byte(replacedContent), 0)
if err != nil {

View File

@@ -13,6 +13,8 @@ import (
"strings"
"time"
"mizuserver/pkg/servicemap"
"github.com/google/martian/har"
"github.com/up9inc/mizu/shared"
"github.com/up9inc/mizu/shared/logger"
@@ -146,6 +148,8 @@ func startReadingChannel(outputItems <-chan *tapApi.OutputChannelItem, extension
panic(err)
}
connection.SendText(string(data))
servicemap.GetInstance().NewTCPEntry(mizuEntry.Source, mizuEntry.Destination, &item.Protocol)
}
}

View File

@@ -0,0 +1,36 @@
package controllers
import (
"mizuserver/pkg/servicemap"
"net/http"
"github.com/gin-gonic/gin"
)
type ServiceMapController struct {
service servicemap.ServiceMap
}
func NewServiceMapController() *ServiceMapController {
return &ServiceMapController{
service: servicemap.GetInstance(),
}
}
func (s *ServiceMapController) Status(c *gin.Context) {
c.JSON(http.StatusOK, s.service.GetStatus())
}
func (s *ServiceMapController) Get(c *gin.Context) {
response := &servicemap.ServiceMapResponse{
Status: s.service.GetStatus(),
Nodes: s.service.GetNodes(),
Edges: s.service.GetEdges(),
}
c.JSON(http.StatusOK, response)
}
func (s *ServiceMapController) Reset(c *gin.Context) {
s.service.Reset()
s.Status(c)
}

View File

@@ -0,0 +1,147 @@
package controllers
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"mizuserver/pkg/servicemap"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/suite"
"github.com/up9inc/mizu/shared"
tapApi "github.com/up9inc/mizu/tap/api"
)
const (
a = "aService"
b = "bService"
Ip = "127.0.0.1"
Port = "80"
)
var (
TCPEntryA = &tapApi.TCP{
Name: a,
Port: Port,
IP: fmt.Sprintf("%s.%s", Ip, a),
}
TCPEntryB = &tapApi.TCP{
Name: b,
Port: Port,
IP: fmt.Sprintf("%s.%s", Ip, b),
}
)
var ProtocolHttp = &tapApi.Protocol{
Name: "http",
LongName: "Hypertext Transfer Protocol -- HTTP/1.1",
Abbreviation: "HTTP",
Macro: "http",
Version: "1.1",
BackgroundColor: "#205cf5",
ForegroundColor: "#ffffff",
FontSize: 12,
ReferenceLink: "https://datatracker.ietf.org/doc/html/rfc2616",
Ports: []string{"80", "443", "8080"},
Priority: 0,
}
type ServiceMapControllerSuite struct {
suite.Suite
c *ServiceMapController
w *httptest.ResponseRecorder
g *gin.Context
}
func (s *ServiceMapControllerSuite) SetupTest() {
s.c = NewServiceMapController()
s.c.service.SetConfig(&shared.MizuAgentConfig{
ServiceMap: true,
})
s.c.service.NewTCPEntry(TCPEntryA, TCPEntryB, ProtocolHttp)
s.w = httptest.NewRecorder()
s.g, _ = gin.CreateTestContext(s.w)
}
func (s *ServiceMapControllerSuite) TestGetStatus() {
assert := s.Assert()
s.c.Status(s.g)
assert.Equal(http.StatusOK, s.w.Code)
var status servicemap.ServiceMapStatus
err := json.Unmarshal(s.w.Body.Bytes(), &status)
assert.NoError(err)
assert.Equal("enabled", status.Status)
assert.Equal(1, status.EntriesProcessedCount)
assert.Equal(2, status.NodeCount)
assert.Equal(1, status.EdgeCount)
}
func (s *ServiceMapControllerSuite) TestGet() {
assert := s.Assert()
s.c.Get(s.g)
assert.Equal(http.StatusOK, s.w.Code)
var response servicemap.ServiceMapResponse
err := json.Unmarshal(s.w.Body.Bytes(), &response)
assert.NoError(err)
// response status
assert.Equal("enabled", response.Status.Status)
assert.Equal(1, response.Status.EntriesProcessedCount)
assert.Equal(2, response.Status.NodeCount)
assert.Equal(1, response.Status.EdgeCount)
// response nodes
aNode := servicemap.ServiceMapNode{
Id: 1,
Name: TCPEntryA.IP,
Entry: TCPEntryA,
Count: 1,
}
bNode := servicemap.ServiceMapNode{
Id: 2,
Name: TCPEntryB.IP,
Entry: TCPEntryB,
Count: 1,
}
assert.Contains(response.Nodes, aNode)
assert.Contains(response.Nodes, bNode)
assert.Len(response.Nodes, 2)
// response edges
assert.Equal([]servicemap.ServiceMapEdge{
{
Source: aNode,
Destination: bNode,
Protocol: ProtocolHttp,
Count: 1,
},
}, response.Edges)
}
func (s *ServiceMapControllerSuite) TestGetReset() {
assert := s.Assert()
s.c.Reset(s.g)
assert.Equal(http.StatusOK, s.w.Code)
var status servicemap.ServiceMapStatus
err := json.Unmarshal(s.w.Body.Bytes(), &status)
assert.NoError(err)
assert.Equal("enabled", status.Status)
assert.Equal(0, status.EntriesProcessedCount)
assert.Equal(0, status.NodeCount)
assert.Equal(0, status.EdgeCount)
}
func TestServiceMapControllerSuite(t *testing.T) {
suite.Run(t, new(ServiceMapControllerSuite))
}

View File

@@ -0,0 +1,19 @@
package routes
import (
"mizuserver/pkg/controllers"
"mizuserver/pkg/middlewares"
"github.com/gin-gonic/gin"
)
func ServiceMapRoutes(ginApp *gin.Engine) {
routeGroup := ginApp.Group("/servicemap")
routeGroup.Use(middlewares.RequiresAuth())
controller := controllers.NewServiceMapController()
routeGroup.GET("/status", controller.Status)
routeGroup.GET("/get", controller.Get)
routeGroup.GET("/reset", controller.Reset)
}

View File

@@ -0,0 +1,32 @@
package servicemap
import (
tapApi "github.com/up9inc/mizu/tap/api"
)
type ServiceMapStatus struct {
Status string `json:"status"`
EntriesProcessedCount int `json:"entriesProcessedCount"`
NodeCount int `json:"nodeCount"`
EdgeCount int `json:"edgeCount"`
}
type ServiceMapResponse struct {
Status ServiceMapStatus `json:"status"`
Nodes []ServiceMapNode `json:"nodes"`
Edges []ServiceMapEdge `json:"edges"`
}
type ServiceMapNode struct {
Id int `json:"id"`
Name string `json:"name"`
Entry *tapApi.TCP `json:"entry"`
Count int `json:"count"`
}
type ServiceMapEdge struct {
Source ServiceMapNode `json:"source"`
Destination ServiceMapNode `json:"destination"`
Count int `json:"count"`
Protocol *tapApi.Protocol `json:"protocol"`
}

View File

@@ -0,0 +1,271 @@
package servicemap
import (
"sync"
"github.com/up9inc/mizu/shared"
"github.com/up9inc/mizu/shared/logger"
tapApi "github.com/up9inc/mizu/tap/api"
)
const (
ServiceMapEnabled = "enabled"
ServiceMapDisabled = "disabled"
UnresolvedNodeName = "unresolved"
)
var instance *serviceMap
var once sync.Once
func GetInstance() ServiceMap {
once.Do(func() {
instance = newServiceMap()
logger.Log.Debug("Service Map Initialized")
})
return instance
}
type serviceMap struct {
config *shared.MizuAgentConfig
graph *graph
entriesProcessed int
}
type ServiceMap interface {
SetConfig(config *shared.MizuAgentConfig)
IsEnabled() bool
NewTCPEntry(source *tapApi.TCP, destination *tapApi.TCP, protocol *tapApi.Protocol)
GetStatus() ServiceMapStatus
GetNodes() []ServiceMapNode
GetEdges() []ServiceMapEdge
GetEntriesProcessedCount() int
GetNodesCount() int
GetEdgesCount() int
Reset()
}
func newServiceMap() *serviceMap {
return &serviceMap{
config: nil,
entriesProcessed: 0,
graph: newDirectedGraph(),
}
}
type key string
type entryData struct {
key key
entry *tapApi.TCP
}
type nodeData struct {
id int
entry *tapApi.TCP
count int
}
type edgeProtocol struct {
protocol *tapApi.Protocol
count int
}
type edgeData struct {
data map[key]*edgeProtocol
}
type graph struct {
Nodes map[key]*nodeData
Edges map[key]map[key]*edgeData
}
func newDirectedGraph() *graph {
return &graph{
Nodes: make(map[key]*nodeData),
Edges: make(map[key]map[key]*edgeData),
}
}
func newNodeData(id int, e *tapApi.TCP) *nodeData {
return &nodeData{
id: id,
entry: e,
count: 1,
}
}
func newEdgeData(p *tapApi.Protocol) *edgeData {
return &edgeData{
data: map[key]*edgeProtocol{
key(p.Name): {
protocol: p,
count: 1,
},
},
}
}
func (s *serviceMap) nodeExists(k key) (*nodeData, bool) {
n, ok := s.graph.Nodes[k]
return n, ok
}
func (s *serviceMap) addNode(k key, e *tapApi.TCP) (*nodeData, bool) {
nd, exists := s.nodeExists(k)
if !exists {
s.graph.Nodes[k] = newNodeData(len(s.graph.Nodes)+1, e)
return s.graph.Nodes[k], true
}
return nd, false
}
func (s *serviceMap) addEdge(u, v *entryData, p *tapApi.Protocol) {
if n, ok := s.addNode(u.key, u.entry); !ok {
n.count++
}
if n, ok := s.addNode(v.key, v.entry); !ok {
n.count++
}
if _, ok := s.graph.Edges[u.key]; !ok {
s.graph.Edges[u.key] = make(map[key]*edgeData)
}
// new edge u -> v pair
// protocol is the same for u and v
if e, ok := s.graph.Edges[u.key][v.key]; ok {
// edge data already exists for u -> v pair
// we have a new protocol for this u -> v pair
k := key(p.Name)
if pd, pOk := e.data[k]; pOk {
// protocol key already exists, just increment the count
pd.count++
} else {
// new protocol key
e.data[k] = &edgeProtocol{
protocol: p,
count: 1,
}
}
} else {
// new edge data for u -> v pair
s.graph.Edges[u.key][v.key] = newEdgeData(p)
}
s.entriesProcessed++
}
func (s *serviceMap) SetConfig(config *shared.MizuAgentConfig) {
s.config = config
}
func (s *serviceMap) IsEnabled() bool {
if s.config != nil && s.config.ServiceMap {
return true
}
return false
}
func (s *serviceMap) NewTCPEntry(src *tapApi.TCP, dst *tapApi.TCP, p *tapApi.Protocol) {
if !s.IsEnabled() {
return
}
srcEntry := &entryData{
key: key(src.IP),
entry: src,
}
if len(srcEntry.entry.Name) == 0 {
srcEntry.entry.Name = UnresolvedNodeName
}
dstEntry := &entryData{
key: key(dst.IP),
entry: dst,
}
if len(dstEntry.entry.Name) == 0 {
dstEntry.entry.Name = UnresolvedNodeName
}
s.addEdge(srcEntry, dstEntry, p)
}
func (s *serviceMap) GetStatus() ServiceMapStatus {
status := ServiceMapDisabled
if s.IsEnabled() {
status = ServiceMapEnabled
}
return ServiceMapStatus{
Status: status,
EntriesProcessedCount: s.entriesProcessed,
NodeCount: s.GetNodesCount(),
EdgeCount: s.GetEdgesCount(),
}
}
func (s *serviceMap) GetNodes() []ServiceMapNode {
var nodes []ServiceMapNode
for i, n := range s.graph.Nodes {
nodes = append(nodes, ServiceMapNode{
Id: n.id,
Name: string(i),
Entry: n.entry,
Count: n.count,
})
}
return nodes
}
func (s *serviceMap) GetEdges() []ServiceMapEdge {
var edges []ServiceMapEdge
for u, m := range s.graph.Edges {
for v := range m {
for _, p := range s.graph.Edges[u][v].data {
edges = append(edges, ServiceMapEdge{
Source: ServiceMapNode{
Id: s.graph.Nodes[u].id,
Name: string(u),
Entry: s.graph.Nodes[u].entry,
Count: s.graph.Nodes[u].count,
},
Destination: ServiceMapNode{
Id: s.graph.Nodes[v].id,
Name: string(v),
Entry: s.graph.Nodes[v].entry,
Count: s.graph.Nodes[v].count,
},
Count: p.count,
Protocol: p.protocol,
})
}
}
}
return edges
}
func (s *serviceMap) GetEntriesProcessedCount() int {
return s.entriesProcessed
}
func (s *serviceMap) GetNodesCount() int {
return len(s.graph.Nodes)
}
func (s *serviceMap) GetEdgesCount() int {
var count int
for u, m := range s.graph.Edges {
for v := range m {
for range s.graph.Edges[u][v].data {
count++
}
}
}
return count
}
func (s *serviceMap) Reset() {
s.entriesProcessed = 0
s.graph = newDirectedGraph()
}

View File

@@ -0,0 +1,405 @@
package servicemap
import (
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/suite"
"github.com/up9inc/mizu/shared"
tapApi "github.com/up9inc/mizu/tap/api"
)
const (
a = "aService"
b = "bService"
c = "cService"
d = "dService"
Ip = "127.0.0.1"
Port = "80"
)
var (
TCPEntryA = &tapApi.TCP{
Name: a,
Port: Port,
IP: fmt.Sprintf("%s.%s", Ip, a),
}
TCPEntryB = &tapApi.TCP{
Name: b,
Port: Port,
IP: fmt.Sprintf("%s.%s", Ip, b),
}
TCPEntryC = &tapApi.TCP{
Name: c,
Port: Port,
IP: fmt.Sprintf("%s.%s", Ip, c),
}
TCPEntryD = &tapApi.TCP{
Name: d,
Port: Port,
IP: fmt.Sprintf("%s.%s", Ip, d),
}
TCPEntryUnresolved = &tapApi.TCP{
Name: "",
Port: Port,
IP: Ip,
}
TCPEntryUnresolved2 = &tapApi.TCP{
Name: "",
Port: Port,
IP: fmt.Sprintf("%s.%s", Ip, UnresolvedNodeName),
}
ProtocolHttp = &tapApi.Protocol{
Name: "http",
LongName: "Hypertext Transfer Protocol -- HTTP/1.1",
Abbreviation: "HTTP",
Macro: "http",
Version: "1.1",
BackgroundColor: "#205cf5",
ForegroundColor: "#ffffff",
FontSize: 12,
ReferenceLink: "https://datatracker.ietf.org/doc/html/rfc2616",
Ports: []string{"80", "443", "8080"},
Priority: 0,
}
ProtocolRedis = &tapApi.Protocol{
Name: "redis",
LongName: "Redis Serialization Protocol",
Abbreviation: "REDIS",
Macro: "redis",
Version: "3.x",
BackgroundColor: "#a41e11",
ForegroundColor: "#ffffff",
FontSize: 11,
ReferenceLink: "https://redis.io/topics/protocol",
Ports: []string{"6379"},
Priority: 3,
}
)
type ServiceMapDisabledSuite struct {
suite.Suite
instance ServiceMap
}
type ServiceMapEnabledSuite struct {
suite.Suite
instance ServiceMap
}
func (s *ServiceMapDisabledSuite) SetupTest() {
s.instance = GetInstance()
}
func (s *ServiceMapEnabledSuite) SetupTest() {
s.instance = GetInstance()
s.instance.SetConfig(&shared.MizuAgentConfig{
ServiceMap: true,
})
}
func (s *ServiceMapDisabledSuite) TestServiceMapInstance() {
assert := s.Assert()
assert.NotNil(s.instance)
}
func (s *ServiceMapDisabledSuite) TestServiceMapSingletonInstance() {
assert := s.Assert()
instance2 := GetInstance()
assert.NotNil(s.instance)
assert.NotNil(instance2)
assert.Equal(s.instance, instance2)
}
func (s *ServiceMapDisabledSuite) TestServiceMapIsEnabledShouldReturnFalseByDefault() {
assert := s.Assert()
enabled := s.instance.IsEnabled()
assert.False(enabled)
}
func (s *ServiceMapDisabledSuite) TestGetStatusShouldReturnDisabledByDefault() {
assert := s.Assert()
status := s.instance.GetStatus()
assert.Equal("disabled", status.Status)
assert.Equal(0, status.EntriesProcessedCount)
assert.Equal(0, status.NodeCount)
assert.Equal(0, status.EdgeCount)
}
func (s *ServiceMapDisabledSuite) TestNewTCPEntryShouldDoNothingWhenDisabled() {
assert := s.Assert()
s.instance.NewTCPEntry(TCPEntryA, TCPEntryB, ProtocolHttp)
s.instance.NewTCPEntry(TCPEntryC, TCPEntryD, ProtocolHttp)
status := s.instance.GetStatus()
assert.Equal("disabled", status.Status)
assert.Equal(0, status.EntriesProcessedCount)
assert.Equal(0, status.NodeCount)
assert.Equal(0, status.EdgeCount)
}
// Enabled
func (s *ServiceMapEnabledSuite) TestServiceMapIsEnabled() {
assert := s.Assert()
enabled := s.instance.IsEnabled()
assert.True(enabled)
}
func (s *ServiceMapEnabledSuite) TestServiceMap() {
assert := s.Assert()
// A -> B - HTTP
s.instance.NewTCPEntry(TCPEntryA, TCPEntryB, ProtocolHttp)
nodes := s.instance.GetNodes()
edges := s.instance.GetEdges()
// Counts for the first entry
assert.Equal(1, s.instance.GetEntriesProcessedCount())
assert.Equal(2, s.instance.GetNodesCount())
assert.Equal(2, len(nodes))
assert.Equal(1, s.instance.GetEdgesCount())
assert.Equal(1, len(edges))
//http protocol
assert.Equal(1, edges[0].Count)
assert.Equal(ProtocolHttp.Name, edges[0].Protocol.Name)
// same A -> B - HTTP, http protocol count should be 2, edges count should be 1
s.instance.NewTCPEntry(TCPEntryA, TCPEntryB, ProtocolHttp)
nodes = s.instance.GetNodes()
edges = s.instance.GetEdges()
// Counts for a second entry
assert.Equal(2, s.instance.GetEntriesProcessedCount())
assert.Equal(2, s.instance.GetNodesCount())
assert.Equal(2, len(nodes))
// edges count should still be 1, but http protocol count should be 2
assert.Equal(1, s.instance.GetEdgesCount())
assert.Equal(1, len(edges))
// http protocol
assert.Equal(2, edges[0].Count) //http
assert.Equal(ProtocolHttp.Name, edges[0].Protocol.Name)
// same A -> B - REDIS, http protocol count should be 2 and redis protocol count should 1, edges count should be 2
s.instance.NewTCPEntry(TCPEntryA, TCPEntryB, ProtocolRedis)
nodes = s.instance.GetNodes()
edges = s.instance.GetEdges()
// Counts after second entry
assert.Equal(3, s.instance.GetEntriesProcessedCount())
assert.Equal(2, s.instance.GetNodesCount())
assert.Equal(2, len(nodes))
// edges count should be 2, http protocol count should be 2 and redis protocol should be 1
assert.Equal(2, s.instance.GetEdgesCount())
assert.Equal(2, len(edges))
// http and redis protocols
httpIndex := -1
redisIndex := -1
for i, e := range edges {
if e.Protocol.Name == ProtocolHttp.Name {
httpIndex = i
continue
}
if e.Protocol.Name == ProtocolRedis.Name {
redisIndex = i
}
}
assert.NotEqual(-1, httpIndex)
assert.NotEqual(-1, redisIndex)
// http protocol
assert.Equal(2, edges[httpIndex].Count)
assert.Equal(ProtocolHttp.Name, edges[httpIndex].Protocol.Name)
// redis protocol
assert.Equal(1, edges[redisIndex].Count)
assert.Equal(ProtocolRedis.Name, edges[redisIndex].Protocol.Name)
// other entries
s.instance.NewTCPEntry(TCPEntryUnresolved, TCPEntryA, ProtocolHttp)
s.instance.NewTCPEntry(TCPEntryB, TCPEntryUnresolved2, ProtocolHttp)
s.instance.NewTCPEntry(TCPEntryC, TCPEntryD, ProtocolHttp)
s.instance.NewTCPEntry(TCPEntryA, TCPEntryC, ProtocolHttp)
status := s.instance.GetStatus()
nodes = s.instance.GetNodes()
edges = s.instance.GetEdges()
expectedEntriesProcessedCount := 7
expectedNodeCount := 6
expectedEdgeCount := 6
// Counts after all entries
assert.Equal(expectedEntriesProcessedCount, s.instance.GetEntriesProcessedCount())
assert.Equal(expectedNodeCount, s.instance.GetNodesCount())
assert.Equal(expectedNodeCount, len(nodes))
assert.Equal(expectedEdgeCount, s.instance.GetEdgesCount())
assert.Equal(expectedEdgeCount, len(edges))
// Status
assert.Equal("enabled", status.Status)
assert.Equal(expectedEntriesProcessedCount, status.EntriesProcessedCount)
assert.Equal(expectedNodeCount, status.NodeCount)
assert.Equal(expectedEdgeCount, status.EdgeCount)
// Nodes
aNode := -1
bNode := -1
cNode := -1
dNode := -1
unresolvedNode := -1
unresolvedNode2 := -1
var validateNode = func(node ServiceMapNode, entryName string, count int) int {
// id
assert.GreaterOrEqual(node.Id, 1)
assert.LessOrEqual(node.Id, expectedNodeCount)
// entry
// node.Name is the key of the node, key = entry.IP
// entry.Name is the name of the service and could be unresolved
assert.Equal(node.Name, node.Entry.IP)
assert.Equal(Port, node.Entry.Port)
assert.Equal(entryName, node.Entry.Name)
// count
assert.Equal(count, node.Count)
return node.Id
}
for _, v := range nodes {
if strings.HasSuffix(v.Name, a) {
aNode = validateNode(v, a, 5)
continue
}
if strings.HasSuffix(v.Name, b) {
bNode = validateNode(v, b, 4)
continue
}
if strings.HasSuffix(v.Name, c) {
cNode = validateNode(v, c, 2)
continue
}
if strings.HasSuffix(v.Name, d) {
dNode = validateNode(v, d, 1)
continue
}
if v.Name == Ip {
unresolvedNode = validateNode(v, UnresolvedNodeName, 1)
continue
}
if strings.HasSuffix(v.Name, UnresolvedNodeName) {
unresolvedNode2 = validateNode(v, UnresolvedNodeName, 1)
continue
}
}
// Make sure we found all the nodes
nodeIds := [...]int{aNode, bNode, cNode, dNode, unresolvedNode, unresolvedNode2}
for _, v := range nodeIds {
assert.NotEqual(-1, v)
}
// Edges
abEdge := -1
uaEdge := -1
buEdge := -1
cdEdge := -1
acEdge := -1
var validateEdge = func(edge ServiceMapEdge, sourceEntryName string, destEntryName string, protocolName string, protocolCount int) {
// source
assert.Contains(nodeIds, edge.Source.Id)
assert.LessOrEqual(edge.Source.Id, expectedNodeCount)
assert.Equal(edge.Source.Name, edge.Source.Entry.IP)
assert.Equal(sourceEntryName, edge.Source.Entry.Name)
// destination
assert.Contains(nodeIds, edge.Destination.Id)
assert.LessOrEqual(edge.Destination.Id, expectedNodeCount)
assert.Equal(edge.Destination.Name, edge.Destination.Entry.IP)
assert.Equal(destEntryName, edge.Destination.Entry.Name)
// protocol
assert.Equal(protocolName, edge.Protocol.Name)
assert.Equal(protocolCount, edge.Count)
}
for i, v := range edges {
if v.Source.Entry.Name == a && v.Destination.Entry.Name == b && v.Protocol.Name == "http" {
validateEdge(v, a, b, ProtocolHttp.Name, 2)
abEdge = i
continue
}
if v.Source.Entry.Name == a && v.Destination.Entry.Name == b && v.Protocol.Name == "redis" {
validateEdge(v, a, b, ProtocolRedis.Name, 1)
abEdge = i
continue
}
if v.Source.Entry.Name == UnresolvedNodeName && v.Destination.Entry.Name == a {
validateEdge(v, UnresolvedNodeName, a, ProtocolHttp.Name, 1)
uaEdge = i
continue
}
if v.Source.Entry.Name == b && v.Destination.Entry.Name == UnresolvedNodeName {
validateEdge(v, b, UnresolvedNodeName, ProtocolHttp.Name, 1)
buEdge = i
continue
}
if v.Source.Entry.Name == c && v.Destination.Entry.Name == d {
validateEdge(v, c, d, ProtocolHttp.Name, 1)
cdEdge = i
continue
}
if v.Source.Entry.Name == a && v.Destination.Entry.Name == c {
validateEdge(v, a, c, ProtocolHttp.Name, 1)
acEdge = i
continue
}
}
// Make sure we found all the edges
for _, v := range [...]int{abEdge, uaEdge, buEdge, cdEdge, acEdge} {
assert.NotEqual(-1, v)
}
// Reset
s.instance.Reset()
status = s.instance.GetStatus()
nodes = s.instance.GetNodes()
edges = s.instance.GetEdges()
// Counts after reset
assert.Equal(0, s.instance.GetEntriesProcessedCount())
assert.Equal(0, s.instance.GetNodesCount())
assert.Equal(0, s.instance.GetEdgesCount())
// Status after reset
assert.Equal("enabled", status.Status)
assert.Equal(0, status.EntriesProcessedCount)
assert.Equal(0, status.NodeCount)
assert.Equal(0, status.EdgeCount)
// Nodes after reset
assert.Equal([]ServiceMapNode(nil), nodes)
// Edges after reset
assert.Equal([]ServiceMapEdge(nil), edges)
}
func TestServiceMapSuite(t *testing.T) {
suite.Run(t, new(ServiceMapDisabledSuite))
suite.Run(t, new(ServiceMapEnabledSuite))
}

View File

@@ -36,6 +36,16 @@ func NewProvider(url string, retries int, timeout time.Duration) *Provider {
}
}
func NewProviderWithoutRetries(url string, timeout time.Duration) *Provider {
return &Provider{
url: url,
retries: 1,
client: &http.Client{
Timeout: timeout,
},
}
}
func (provider *Provider) TestConnection() error {
retriesLeft := provider.retries
for retriesLeft > 0 {

View File

@@ -6,18 +6,18 @@ import (
"errors"
"fmt"
"github.com/up9inc/mizu/cli/apiserver"
"github.com/up9inc/mizu/cli/config/configStructs"
"github.com/up9inc/mizu/cli/errormessage"
"github.com/up9inc/mizu/cli/mizu"
"github.com/up9inc/mizu/cli/mizu/fsUtils"
"github.com/up9inc/mizu/cli/resources"
"github.com/up9inc/mizu/cli/telemetry"
"github.com/up9inc/mizu/cli/uiUtils"
"github.com/up9inc/mizu/shared"
"path"
"time"
"github.com/up9inc/mizu/cli/config"
"github.com/up9inc/mizu/cli/config/configStructs"
"github.com/up9inc/mizu/cli/errormessage"
"github.com/up9inc/mizu/cli/uiUtils"
"github.com/up9inc/mizu/shared/kubernetes"
"github.com/up9inc/mizu/shared/logger"
)
@@ -27,14 +27,35 @@ func GetApiServerUrl() string {
}
func startProxyReportErrorIfAny(kubernetesProvider *kubernetes.Provider, cancel context.CancelFunc) {
err := kubernetes.StartProxy(kubernetesProvider, config.Config.Tap.ProxyHost, config.Config.Tap.GuiPort, config.Config.MizuResourcesNamespace, kubernetes.ApiServerPodName)
httpServer, err := kubernetes.StartProxy(kubernetesProvider, config.Config.Tap.ProxyHost, config.Config.Tap.GuiPort, config.Config.MizuResourcesNamespace, kubernetes.ApiServerPodName, cancel)
if err != nil {
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error occured while running k8s proxy %v\n"+
"Try setting different port by using --%s", errormessage.FormatError(err), configStructs.GuiPortTapName))
cancel()
return
}
logger.Log.Debugf("proxy ended")
apiProvider = apiserver.NewProviderWithoutRetries(GetApiServerUrl(), time.Second) // short check for proxy
if err := apiProvider.TestConnection(); err != nil {
logger.Log.Debugf("Couldn't connect using proxy, stopping proxy and trying to create port-forward")
if err := httpServer.Shutdown(context.Background()); err != nil {
logger.Log.Debugf("Error occurred while stopping proxy %v", errormessage.FormatError(err))
}
if err := kubernetes.NewPortForward(kubernetesProvider, config.Config.MizuResourcesNamespace, kubernetes.ApiServerPodName, config.Config.Tap.GuiPort, cancel); err != nil {
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error occured while running port forward %v\n"+
"Try setting different port by using --%s", errormessage.FormatError(err), configStructs.GuiPortTapName))
cancel()
return
}
apiProvider = apiserver.NewProvider(GetApiServerUrl(), apiserver.DefaultRetries, apiserver.DefaultTimeout) // long check for port-forward
if err := apiProvider.TestConnection(); err != nil {
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Couldn't connect to API server, for more info check logs at %s", fsUtils.GetLogFilePath()))
cancel()
return
}
}
}
func getKubernetesProviderForCli() (*kubernetes.Provider, error) {

View File

@@ -46,7 +46,8 @@ func runMizuInstall() {
if err = resources.CreateInstallMizuResources(ctx, kubernetesProvider, serializedValidationRules,
serializedContract, serializedMizuConfig, config.Config.IsNsRestrictedMode(),
config.Config.MizuResourcesNamespace, config.Config.AgentImage,
config.Config.MizuResourcesNamespace, config.Config.AgentImage, config.Config.BasenineImage,
config.Config.KratosImage,
nil, defaultMaxEntriesDBSizeBytes, defaultResources, config.Config.ImagePullPolicy(),
config.Config.LogLevel(), false); err != nil {
var statusError *k8serrors.StatusError

View File

@@ -23,7 +23,6 @@ import (
"github.com/up9inc/mizu/cli/config"
"github.com/up9inc/mizu/cli/config/configStructs"
"github.com/up9inc/mizu/cli/errormessage"
"github.com/up9inc/mizu/cli/mizu/fsUtils"
"github.com/up9inc/mizu/cli/uiUtils"
"github.com/up9inc/mizu/shared"
"github.com/up9inc/mizu/shared/kubernetes"
@@ -124,7 +123,7 @@ func RunMizuTap() {
}
logger.Log.Infof("Waiting for Mizu Agent to start...")
if state.mizuServiceAccountExists, err = resources.CreateTapMizuResources(ctx, kubernetesProvider, serializedValidationRules, serializedContract, serializedMizuConfig, config.Config.IsNsRestrictedMode(), config.Config.MizuResourcesNamespace, config.Config.AgentImage, getSyncEntriesConfig(), config.Config.Tap.MaxEntriesDBSizeBytes(), config.Config.Tap.ApiServerResources, config.Config.ImagePullPolicy(), config.Config.LogLevel()); err != nil {
if state.mizuServiceAccountExists, err = resources.CreateTapMizuResources(ctx, kubernetesProvider, serializedValidationRules, serializedContract, serializedMizuConfig, config.Config.IsNsRestrictedMode(), config.Config.MizuResourcesNamespace, config.Config.AgentImage, config.Config.BasenineImage, getSyncEntriesConfig(), config.Config.Tap.MaxEntriesDBSizeBytes(), config.Config.Tap.ApiServerResources, config.Config.ImagePullPolicy(), config.Config.LogLevel()); err != nil {
var statusError *k8serrors.StatusError
if errors.As(err, &statusError) {
if statusError.ErrStatus.Reason == metav1.StatusReasonAlreadyExists {
@@ -416,20 +415,15 @@ func watchApiServerEvents(ctx context.Context, kubernetesProvider *kubernetes.Pr
}
func postApiServerStarted(ctx context.Context, kubernetesProvider *kubernetes.Provider, cancel context.CancelFunc, err error) {
go startProxyReportErrorIfAny(kubernetesProvider, cancel)
startProxyReportErrorIfAny(kubernetesProvider, cancel)
url := GetApiServerUrl()
if err := apiProvider.TestConnection(); err != nil {
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Couldn't connect to API server, for more info check logs at %s", fsUtils.GetLogFilePath()))
cancel()
return
}
options, _ := getMizuApiFilteringOptions()
if err = startTapperSyncer(ctx, cancel, kubernetesProvider, state.targetNamespaces, *options, state.startTime); err != nil {
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error starting mizu tapper syncer: %v", err))
cancel()
}
url := GetApiServerUrl()
logger.Log.Infof("Mizu is available at %s", url)
if !config.Config.HeadlessMode {
uiUtils.OpenBrowser(url)

View File

@@ -47,8 +47,7 @@ func runMizuView() {
return
}
logger.Log.Infof("Establishing connection to k8s cluster...")
go startProxyReportErrorIfAny(kubernetesProvider, cancel)
startProxyReportErrorIfAny(kubernetesProvider, cancel)
}
apiServerProvider := apiserver.NewProvider(url, apiserver.DefaultRetries, apiserver.DefaultTimeout)

View File

@@ -5,6 +5,7 @@ import (
"github.com/op/go-logging"
"github.com/up9inc/mizu/cli/config/configStructs"
"github.com/up9inc/mizu/cli/mizu"
"github.com/up9inc/mizu/shared"
v1 "k8s.io/api/core/v1"
"k8s.io/client-go/util/homedir"
"os"
@@ -26,6 +27,8 @@ type ConfigStruct struct {
Auth configStructs.AuthConfig `yaml:"auth"`
Config configStructs.ConfigConfig `yaml:"config,omitempty"`
AgentImage string `yaml:"agent-image,omitempty" readonly:""`
BasenineImage string `yaml:"basenine-image,omitempty" readonly:""`
KratosImage string `yaml:"kratos-image,omitempty" readonly:""`
ImagePullPolicyStr string `yaml:"image-pull-policy" default:"Always"`
MizuResourcesNamespace string `yaml:"mizu-resources-namespace" default:"mizu"`
Telemetry bool `yaml:"telemetry" default:"true"`
@@ -47,6 +50,8 @@ func (config *ConfigStruct) validate() error {
}
func (config *ConfigStruct) SetDefaults() {
config.BasenineImage = fmt.Sprintf("%s:%s", shared.BasenineImageRepo, shared.BasenineImageTag)
config.KratosImage = shared.KratosImageDefault
config.AgentImage = fmt.Sprintf("gcr.io/up9-docker-hub/mizu/%s:%s", mizu.Branch, mizu.SemVer)
config.ConfigFilePath = path.Join(mizu.GetMizuFolderPath(), "config.yaml")
}

View File

@@ -100,6 +100,7 @@ github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc=
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
@@ -336,6 +337,7 @@ github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS4
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8=
github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=

View File

@@ -15,7 +15,7 @@ import (
"k8s.io/apimachinery/pkg/util/intstr"
)
func CreateTapMizuResources(ctx context.Context, kubernetesProvider *kubernetes.Provider, serializedValidationRules string, serializedContract string, serializedMizuConfig string, isNsRestrictedMode bool, mizuResourcesNamespace string, agentImage string, syncEntriesConfig *shared.SyncEntriesConfig, maxEntriesDBSizeBytes int64, apiServerResources shared.Resources, imagePullPolicy core.PullPolicy, logLevel logging.Level) (bool, error) {
func CreateTapMizuResources(ctx context.Context, kubernetesProvider *kubernetes.Provider, serializedValidationRules string, serializedContract string, serializedMizuConfig string, isNsRestrictedMode bool, mizuResourcesNamespace string, agentImage string, basenineImage string, syncEntriesConfig *shared.SyncEntriesConfig, maxEntriesDBSizeBytes int64, apiServerResources shared.Resources, imagePullPolicy core.PullPolicy, logLevel logging.Level) (bool, error) {
if !isNsRestrictedMode {
if err := createMizuNamespace(ctx, kubernetesProvider, mizuResourcesNamespace); err != nil {
return false, err
@@ -42,6 +42,8 @@ func CreateTapMizuResources(ctx context.Context, kubernetesProvider *kubernetes.
Namespace: mizuResourcesNamespace,
PodName: kubernetes.ApiServerPodName,
PodImage: agentImage,
BasenineImage: basenineImage,
KratosImage: "",
ServiceAccountName: serviceAccountName,
IsNamespaceRestricted: isNsRestrictedMode,
SyncEntriesConfig: syncEntriesConfig,
@@ -65,7 +67,7 @@ func CreateTapMizuResources(ctx context.Context, kubernetesProvider *kubernetes.
return mizuServiceAccountExists, nil
}
func CreateInstallMizuResources(ctx context.Context, kubernetesProvider *kubernetes.Provider, serializedValidationRules string, serializedContract string, serializedMizuConfig string, isNsRestrictedMode bool, mizuResourcesNamespace string, agentImage string, syncEntriesConfig *shared.SyncEntriesConfig, maxEntriesDBSizeBytes int64, apiServerResources shared.Resources, imagePullPolicy core.PullPolicy, logLevel logging.Level, noPersistentVolumeClaim bool) error {
func CreateInstallMizuResources(ctx context.Context, kubernetesProvider *kubernetes.Provider, serializedValidationRules string, serializedContract string, serializedMizuConfig string, isNsRestrictedMode bool, mizuResourcesNamespace string, agentImage string, basenineImage string, kratosImage string, syncEntriesConfig *shared.SyncEntriesConfig, maxEntriesDBSizeBytes int64, apiServerResources shared.Resources, imagePullPolicy core.PullPolicy, logLevel logging.Level, noPersistentVolumeClaim bool) error {
if err := createMizuNamespace(ctx, kubernetesProvider, mizuResourcesNamespace); err != nil {
return err
}
@@ -95,6 +97,8 @@ func CreateInstallMizuResources(ctx context.Context, kubernetesProvider *kuberne
Namespace: mizuResourcesNamespace,
PodName: kubernetes.ApiServerPodName,
PodImage: agentImage,
BasenineImage: basenineImage,
KratosImage: kratosImage,
ServiceAccountName: serviceAccountName,
IsNamespaceRestricted: isNsRestrictedMode,
SyncEntriesConfig: syncEntriesConfig,
@@ -113,7 +117,7 @@ func CreateInstallMizuResources(ctx context.Context, kubernetesProvider *kuberne
if err != nil {
return err
}
logger.Log.Infof("service/%v created", kubernetes.ApiServerPodName)
logger.Log.Infof("service/%v created", kubernetes.ApiServerPodName)
return nil
}

View File

@@ -18,4 +18,5 @@ const (
BaseninePort = "9099"
BasenineImageRepo = "ghcr.io/up9inc/basenine"
BasenineImageTag = "v0.3.0"
KratosImageDefault = "gcr.io/up9-docker-hub/mizu-kratos/stable:0.0.0"
)

View File

@@ -177,6 +177,8 @@ type ApiServerOptions struct {
Namespace string
PodName string
PodImage string
BasenineImage string
KratosImage string
ServiceAccountName string
IsNamespaceRestricted bool
SyncEntriesConfig *shared.SyncEntriesConfig
@@ -184,6 +186,7 @@ type ApiServerOptions struct {
Resources shared.Resources
ImagePullPolicy core.PullPolicy
LogLevel logging.Level
}
func (provider *Provider) GetMizuApiServerPodObject(opts *ApiServerOptions, mountVolumeClaim bool, volumeClaimName string, createAuthContainer bool) (*core.Pod, error) {
@@ -280,7 +283,7 @@ func (provider *Provider) GetMizuApiServerPodObject(opts *ApiServerOptions, moun
},
{
Name: "basenine",
Image: fmt.Sprintf("%s:%s", shared.BasenineImageRepo, shared.BasenineImageTag),
Image: opts.BasenineImage,
ImagePullPolicy: opts.ImagePullPolicy,
VolumeMounts: volumeMounts,
ReadinessProbe: &core.Probe{
@@ -313,7 +316,7 @@ func (provider *Provider) GetMizuApiServerPodObject(opts *ApiServerOptions, moun
if createAuthContainer {
containers = append(containers, core.Container{
Name: "kratos",
Image: "gcr.io/up9-docker-hub/mizu-kratos/stable:0.0.0",
Image: opts.KratosImage,
ImagePullPolicy: opts.ImagePullPolicy,
VolumeMounts: volumeMounts,
ReadinessProbe: &core.Probe{

View File

@@ -1,9 +1,16 @@
package kubernetes
import (
"bytes"
"context"
"fmt"
"github.com/up9inc/mizu/shared"
"k8s.io/apimachinery/pkg/util/httpstream"
"k8s.io/client-go/tools/portforward"
"k8s.io/client-go/transport/spdy"
"net"
"net/http"
"net/url"
"strings"
"time"
@@ -14,8 +21,8 @@ import (
const k8sProxyApiPrefix = "/"
const mizuServicePort = 80
func StartProxy(kubernetesProvider *Provider, proxyHost string, mizuPort uint16, mizuNamespace string, mizuServiceName string) error {
logger.Log.Debugf("Starting proxy. namespace: [%v], service name: [%s], port: [%v]", mizuNamespace, mizuServiceName, mizuPort)
func StartProxy(kubernetesProvider *Provider, proxyHost string, mizuPort uint16, mizuNamespace string, mizuServiceName string, cancel context.CancelFunc) (*http.Server, error) {
logger.Log.Debugf("Starting proxy using proxy method. namespace: [%v], service name: [%s], port: [%v]", mizuNamespace, mizuServiceName, mizuPort)
filter := &proxy.FilterServer{
AcceptPaths: proxy.MakeRegexpArrayOrDie(proxy.DefaultPathAcceptRE),
RejectPaths: proxy.MakeRegexpArrayOrDie(proxy.DefaultPathRejectRE),
@@ -25,7 +32,7 @@ func StartProxy(kubernetesProvider *Provider, proxyHost string, mizuPort uint16,
proxyHandler, err := proxy.NewProxyHandler(k8sProxyApiPrefix, filter, &kubernetesProvider.clientConfig, time.Second*2)
if err != nil {
return err
return nil, err
}
mux := http.NewServeMux()
mux.Handle(k8sProxyApiPrefix, getRerouteHttpHandlerMizuAPI(proxyHandler, mizuNamespace, mizuServiceName))
@@ -33,14 +40,21 @@ func StartProxy(kubernetesProvider *Provider, proxyHost string, mizuPort uint16,
l, err := net.Listen("tcp", fmt.Sprintf("%s:%d", proxyHost, int(mizuPort)))
if err != nil {
return err
return nil, err
}
server := http.Server{
server := &http.Server{
Handler: mux,
}
return server.Serve(l)
go func() {
if err := server.Serve(l); err != nil && err != http.ErrServerClosed {
logger.Log.Errorf("Error creating proxy, %v", err)
cancel()
}
}()
return server, nil
}
func getMizuApiServerProxiedHostAndPath(mizuNamespace string, mizuServiceName string) string {
@@ -69,3 +83,42 @@ func getRerouteHttpHandlerMizuStatic(proxyHandler http.Handler, mizuNamespace st
proxyHandler.ServeHTTP(w, r)
})
}
func NewPortForward(kubernetesProvider *Provider, namespace string, podName string, localPort uint16, cancel context.CancelFunc) error {
logger.Log.Debugf("Starting proxy using port-forward method. namespace: [%v], service name: [%s], port: [%v]", namespace, podName, localPort)
dialer, err := getHttpDialer(kubernetesProvider, namespace, podName)
if err != nil {
return err
}
stopChan, readyChan := make(chan struct{}, 1), make(chan struct{}, 1)
out, errOut := new(bytes.Buffer), new(bytes.Buffer)
forwarder, err := portforward.New(dialer, []string{fmt.Sprintf("%d:%d", localPort, shared.DefaultApiServerPort)}, stopChan, readyChan, out, errOut)
if err != nil {
return err
}
go func() {
if err = forwarder.ForwardPorts(); err != nil {
logger.Log.Errorf("kubernetes port-forwarding error: %v", err)
cancel()
}
}()
return nil
}
func getHttpDialer(kubernetesProvider *Provider, namespace string, podName string) (httpstream.Dialer, error) {
roundTripper, upgrader, err := spdy.RoundTripperFor(&kubernetesProvider.clientConfig)
if err != nil {
logger.Log.Errorf("Error creating http dialer")
return nil, err
}
path := fmt.Sprintf("/api/v1/namespaces/%s/pods/%s/portforward", namespace, podName)
hostIP := strings.TrimLeft(kubernetesProvider.clientConfig.Host, "htps:/") // no need specify "t" twice
serverURL := url.URL{Scheme: "https", Path: path, Host: hostIP}
return spdy.NewDialer(upgrader, &http.Client{Transport: roundTripper}, http.MethodPost, &serverURL), nil
}

View File

@@ -66,7 +66,7 @@ func handleHTTP2Stream(http2Assembler *Http2Assembler, tcpID *api.TcpID, superTi
streamID,
"HTTP2",
)
item = reqResMatcher.registerRequest(ident, &messageHTTP1, superTimer.CaptureTime)
item = reqResMatcher.registerRequest(ident, &messageHTTP1, superTimer.CaptureTime, messageHTTP1.ProtoMinor)
if item != nil {
item.ConnectionInfo = &api.ConnectionInfo{
ClientIP: tcpID.SrcIP,
@@ -86,7 +86,7 @@ func handleHTTP2Stream(http2Assembler *Http2Assembler, tcpID *api.TcpID, superTi
streamID,
"HTTP2",
)
item = reqResMatcher.registerResponse(ident, &messageHTTP1, superTimer.CaptureTime)
item = reqResMatcher.registerResponse(ident, &messageHTTP1, superTimer.CaptureTime, messageHTTP1.ProtoMinor)
if item != nil {
item.ConnectionInfo = &api.ConnectionInfo{
ClientIP: tcpID.DstIP,
@@ -135,7 +135,7 @@ func handleHTTP1ClientStream(b *bufio.Reader, tcpID *api.TcpID, counterPair *api
counterPair.Request,
"HTTP1",
)
item := reqResMatcher.registerRequest(ident, req, superTimer.CaptureTime)
item := reqResMatcher.registerRequest(ident, req, superTimer.CaptureTime, req.ProtoMinor)
if item != nil {
item.ConnectionInfo = &api.ConnectionInfo{
ClientIP: tcpID.SrcIP,
@@ -175,7 +175,7 @@ func handleHTTP1ServerStream(b *bufio.Reader, tcpID *api.TcpID, counterPair *api
counterPair.Response,
"HTTP1",
)
item := reqResMatcher.registerResponse(ident, res, superTimer.CaptureTime)
item := reqResMatcher.registerResponse(ident, res, superTimer.CaptureTime, res.ProtoMinor)
if item != nil {
item.ConnectionInfo = &api.ConnectionInfo{
ClientIP: tcpID.DstIP,

View File

@@ -235,8 +235,8 @@ func checkIsHTTP2ServerStream(b *bufio.Reader) (bool, error) {
return false, err
}
// If response starts with this text, it is HTTP/1.x
if bytes.Compare(buf, []byte("HTTP/1.0 ")) == 0 || bytes.Compare(buf, []byte("HTTP/1.1 ")) == 0 {
// If response starts with HTTP/1. then it's not HTTP/2
if bytes.HasPrefix(buf, []byte("HTTP/1.")) {
return false, nil
}

View File

@@ -15,7 +15,21 @@ import (
"github.com/up9inc/mizu/tap/api"
)
var protocol api.Protocol = api.Protocol{
var http10protocol api.Protocol = api.Protocol{
Name: "http",
LongName: "Hypertext Transfer Protocol -- HTTP/1.0",
Abbreviation: "HTTP",
Macro: "http",
Version: "1.0",
BackgroundColor: "#205cf5",
ForegroundColor: "#ffffff",
FontSize: 12,
ReferenceLink: "https://datatracker.ietf.org/doc/html/rfc1945",
Ports: []string{"80", "443", "8080"},
Priority: 0,
}
var http11protocol api.Protocol = api.Protocol{
Name: "http",
LongName: "Hypertext Transfer Protocol -- HTTP/1.1",
Abbreviation: "HTTP",
@@ -69,12 +83,12 @@ func init() {
type dissecting string
func (d dissecting) Register(extension *api.Extension) {
extension.Protocol = &protocol
extension.Protocol = &http11protocol
extension.MatcherMap = reqResMatcher.openMessagesMap
}
func (d dissecting) Ping() {
log.Printf("pong %s", protocol.Name)
log.Printf("pong %s", http11protocol.Name)
}
func (d dissecting) Dissect(b *bufio.Reader, isClient bool, tcpID *api.TcpID, counterPair *api.CounterPair, superTimer *api.SuperTimer, superIdentifier *api.SuperIdentifier, emitter api.Emitter, options *api.TrafficFilteringOptions) error {
@@ -96,7 +110,7 @@ func (d dissecting) Dissect(b *bufio.Reader, isClient bool, tcpID *api.TcpID, co
http2Assembler = createHTTP2Assembler(b)
}
if superIdentifier.Protocol != nil && superIdentifier.Protocol != &protocol {
if superIdentifier.Protocol != nil && superIdentifier.Protocol != &http11protocol {
return errors.New("Identified by another protocol")
}
@@ -128,7 +142,7 @@ func (d dissecting) Dissect(b *bufio.Reader, isClient bool, tcpID *api.TcpID, co
tcpID.DstPort,
"HTTP2",
)
item := reqResMatcher.registerRequest(ident, req, superTimer.CaptureTime)
item := reqResMatcher.registerRequest(ident, req, superTimer.CaptureTime, req.ProtoMinor)
if item != nil {
item.ConnectionInfo = &api.ConnectionInfo{
ClientIP: tcpID.SrcIP,
@@ -154,7 +168,7 @@ func (d dissecting) Dissect(b *bufio.Reader, isClient bool, tcpID *api.TcpID, co
if !dissected {
return err
}
superIdentifier.Protocol = &protocol
superIdentifier.Protocol = &http11protocol
return nil
}
@@ -442,9 +456,9 @@ func (d dissecting) Represent(request map[string]interface{}, response map[strin
func (d dissecting) Macros() map[string]string {
return map[string]string{
`http`: fmt.Sprintf(`proto.name == "%s" and proto.version == "%s"`, protocol.Name, protocol.Version),
`http2`: fmt.Sprintf(`proto.name == "%s" and proto.version == "%s"`, protocol.Name, http2Protocol.Version),
`grpc`: fmt.Sprintf(`proto.name == "%s" and proto.version == "%s" and proto.macro == "%s"`, protocol.Name, grpcProtocol.Version, grpcProtocol.Macro),
`http`: fmt.Sprintf(`proto.name == "%s" and proto.version.startsWith("%c")`, http11protocol.Name, http11protocol.Version[0]),
`http2`: fmt.Sprintf(`proto.name == "%s" and proto.version == "%s"`, http11protocol.Name, http2Protocol.Version),
`grpc`: fmt.Sprintf(`proto.name == "%s" and proto.version == "%s" and proto.macro == "%s"`, http11protocol.Name, grpcProtocol.Version, grpcProtocol.Macro),
}
}

View File

@@ -22,7 +22,7 @@ func createResponseRequestMatcher() requestResponseMatcher {
return *newMatcher
}
func (matcher *requestResponseMatcher) registerRequest(ident string, request *http.Request, captureTime time.Time) *api.OutputChannelItem {
func (matcher *requestResponseMatcher) registerRequest(ident string, request *http.Request, captureTime time.Time, protoMinor int) *api.OutputChannelItem {
split := splitIdent(ident)
key := genKey(split)
@@ -41,14 +41,14 @@ func (matcher *requestResponseMatcher) registerRequest(ident string, request *ht
if responseHTTPMessage.IsRequest {
return nil
}
return matcher.preparePair(&requestHTTPMessage, responseHTTPMessage)
return matcher.preparePair(&requestHTTPMessage, responseHTTPMessage, protoMinor)
}
matcher.openMessagesMap.Store(key, &requestHTTPMessage)
return nil
}
func (matcher *requestResponseMatcher) registerResponse(ident string, response *http.Response, captureTime time.Time) *api.OutputChannelItem {
func (matcher *requestResponseMatcher) registerResponse(ident string, response *http.Response, captureTime time.Time, protoMinor int) *api.OutputChannelItem {
split := splitIdent(ident)
key := genKey(split)
@@ -67,14 +67,18 @@ func (matcher *requestResponseMatcher) registerResponse(ident string, response *
if !requestHTTPMessage.IsRequest {
return nil
}
return matcher.preparePair(requestHTTPMessage, &responseHTTPMessage)
return matcher.preparePair(requestHTTPMessage, &responseHTTPMessage, protoMinor)
}
matcher.openMessagesMap.Store(key, &responseHTTPMessage)
return nil
}
func (matcher *requestResponseMatcher) preparePair(requestHTTPMessage *api.GenericMessage, responseHTTPMessage *api.GenericMessage) *api.OutputChannelItem {
func (matcher *requestResponseMatcher) preparePair(requestHTTPMessage *api.GenericMessage, responseHTTPMessage *api.GenericMessage, protoMinor int) *api.OutputChannelItem {
protocol := http11protocol
if protoMinor == 0 {
protocol = http10protocol
}
return &api.OutputChannelItem{
Protocol: protocol,
Timestamp: requestHTTPMessage.CaptureTime.UnixNano() / int64(time.Millisecond),

929
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,22 +15,30 @@
"@types/react-dom": "^17.0.3",
"@uiw/react-textarea-code-editor": "^1.4.12",
"axios": "^0.21.1",
"core-js": "^3.20.2",
"highlight.js": "^11.3.1",
"json-beautify": "^1.1.1",
"jsonpath": "^1.1.1",
"marked": "^4.0.10",
"material-ui-popup-state": "^2.0.0",
"mobx": "^6.3.10",
"moment": "^2.29.1",
"node-fetch": "^3.1.1",
"node-sass": "^5.0.0",
"numeral": "^2.0.6",
"protobuf-decoder": "^0.1.0",
"react": "^17.0.2",
"react-copy-to-clipboard": "^5.0.3",
"react-dom": "^17.0.2",
"react-graph-vis": "^1.0.7",
"react-lowlight": "^3.0.0",
"react-scripts": "4.0.3",
"react-scrollable-feed-virtualized": "^1.4.9",
"react-syntax-highlighter": "^15.4.3",
"react-toastify": "^8.0.3",
"recoil": "^0.5.2",
"redoc": "^2.0.0-rc.59",
"styled-components": "^5.3.3",
"typescript": "^4.2.4",
"web-vitals": "^1.1.1",
"xml-formatter": "^2.6.0"

View File

@@ -30,6 +30,7 @@
// Injected from server
window.isEnt = __IS_STANDALONE__
window.isOasEnabled = __IS_OAS_ENABLED__
window.isServiceMapEnabled = __IS_SERVICE_MAP_ENABLED__
}
catch (e) {
}

View File

@@ -3,6 +3,7 @@ import './App.sass';
import {TLSWarning} from "./components/TLSWarning/TLSWarning";
import {Header} from "./components/Header/Header";
import {TrafficPage} from "./components/TrafficPage";
import { ServiceMapModal } from './components/ServiceMapModal/ServiceMapModal';
const App = () => {
@@ -10,6 +11,7 @@ const App = () => {
const [showTLSWarning, setShowTLSWarning] = useState(false);
const [userDismissedTLSWarning, setUserDismissedTLSWarning] = useState(false);
const [addressesWithTLS, setAddressesWithTLS] = useState(new Set<string>());
const [openServiceMapModal, setOpenServiceMapModal] = useState(false);
const onTLSDetected = (destAddress: string) => {
addressesWithTLS.add(destAddress);
@@ -22,14 +24,19 @@ const App = () => {
return (
<div className="mizuApp">
<Header analyzeStatus={analyzeStatus}/>
<TrafficPage setAnalyzeStatus={setAnalyzeStatus} onTLSDetected={onTLSDetected}/>
<Header analyzeStatus={analyzeStatus} />
<TrafficPage setAnalyzeStatus={setAnalyzeStatus} onTLSDetected={onTLSDetected} setOpenServiceMapModal={setOpenServiceMapModal} />
<TLSWarning showTLSWarning={showTLSWarning}
setShowTLSWarning={setShowTLSWarning}
addressesWithTLS={addressesWithTLS}
setAddressesWithTLS={setAddressesWithTLS}
userDismissedTLSWarning={userDismissedTLSWarning}
setUserDismissedTLSWarning={setUserDismissedTLSWarning}/>
setShowTLSWarning={setShowTLSWarning}
addressesWithTLS={addressesWithTLS}
setAddressesWithTLS={setAddressesWithTLS}
userDismissedTLSWarning={userDismissedTLSWarning}
setUserDismissedTLSWarning={setUserDismissedTLSWarning} />
{window["isServiceMapEnabled"] && <ServiceMapModal
isOpen={openServiceMapModal}
onOpen={() => setOpenServiceMapModal(true)}
onClose={() => setOpenServiceMapModal(false)}
/>}
</div>
);
}

View File

@@ -1,4 +1,4 @@
import React, {useEffect, useState} from 'react';
import React, {useCallback, useEffect, useState} from 'react';
import './App.sass';
import {TrafficPage} from "./components/TrafficPage";
import {TLSWarning} from "./components/TLSWarning/TLSWarning";
@@ -9,43 +9,31 @@ import InstallPage from "./components/InstallPage";
import LoginPage from "./components/LoginPage";
import LoadingOverlay from "./components/LoadingOverlay";
import AuthPageBase from './components/AuthPageBase';
import entPageAtom, {Page} from "./recoil/entPage";
import {useRecoilState} from "recoil";
import { ServiceMapModal } from './components/ServiceMapModal/ServiceMapModal';
const api = Api.getInstance();
// TODO: move to state management
export enum Page {
Traffic,
Setup,
Login
}
// TODO: move to state management
export interface MizuContextModel {
page: Page;
setPage: (page: Page) => void;
}
// TODO: move to state management
export const MizuContext = React.createContext<MizuContextModel>(null);
const EntApp = () => {
const [isLoading, setIsLoading] = useState(true);
const [showTLSWarning, setShowTLSWarning] = useState(false);
const [userDismissedTLSWarning, setUserDismissedTLSWarning] = useState(false);
const [addressesWithTLS, setAddressesWithTLS] = useState(new Set<string>());
const [page, setPage] = useState(Page.Traffic); // TODO: move to state management
const [entPage, setEntPage] = useRecoilState(entPageAtom);
const [isFirstLogin, setIsFirstLogin] = useState(false);
const [openServiceMapModal, setOpenServiceMapModal] = useState(false);
const determinePage = async () => { // TODO: move to state management
const determinePage = useCallback(async () => { // TODO: move to state management
try {
const isInstallNeeded = await api.isInstallNeeded();
if (isInstallNeeded) {
setPage(Page.Setup);
setEntPage(Page.Setup);
} else {
const isAuthNeeded = await api.isAuthenticationNeeded();
if(isAuthNeeded) {
setPage(Page.Login);
setEntPage(Page.Login);
}
}
} catch (e) {
@@ -54,11 +42,11 @@ const EntApp = () => {
} finally {
setIsLoading(false);
}
}
},[setEntPage]);
useEffect(() => {
determinePage();
}, []);
}, [determinePage]);
const onTLSDetected = (destAddress: string) => {
addressesWithTLS.add(destAddress);
@@ -71,9 +59,9 @@ const EntApp = () => {
let pageComponent: any;
switch (page) { // TODO: move to state management / proper routing
switch (entPage) { // TODO: move to state management / proper routing
case Page.Traffic:
pageComponent = <TrafficPage onTLSDetected={onTLSDetected}/>;
pageComponent = <TrafficPage onTLSDetected={onTLSDetected} setOpenServiceMapModal={setOpenServiceMapModal} />;
break;
case Page.Setup:
pageComponent = <AuthPageBase><InstallPage onFirstLogin={() => setIsFirstLogin(true)}/></AuthPageBase>;
@@ -91,16 +79,19 @@ const EntApp = () => {
return (
<div className="mizuApp">
<MizuContext.Provider value={{page, setPage}}>
{page === Page.Traffic && <EntHeader isFirstLogin={isFirstLogin} setIsFirstLogin={setIsFirstLogin}/>}
{pageComponent}
{page === Page.Traffic && <TLSWarning showTLSWarning={showTLSWarning}
setShowTLSWarning={setShowTLSWarning}
addressesWithTLS={addressesWithTLS}
setAddressesWithTLS={setAddressesWithTLS}
userDismissedTLSWarning={userDismissedTLSWarning}
setUserDismissedTLSWarning={setUserDismissedTLSWarning}/>}
</MizuContext.Provider>
{entPage === Page.Traffic && <EntHeader isFirstLogin={isFirstLogin} setIsFirstLogin={setIsFirstLogin} />}
{pageComponent}
{entPage === Page.Traffic && <TLSWarning showTLSWarning={showTLSWarning}
setShowTLSWarning={setShowTLSWarning}
addressesWithTLS={addressesWithTLS}
setAddressesWithTLS={setAddressesWithTLS}
userDismissedTLSWarning={userDismissedTLSWarning}
setUserDismissedTLSWarning={setUserDismissedTLSWarning} />}
{entPage === Page.Traffic && window["isServiceMapEnabled"] && <ServiceMapModal
isOpen={openServiceMapModal}
onOpen={() => setOpenServiceMapModal(true)}
onClose={() => setOpenServiceMapModal(false)}
/>}
</div>
);
}

View File

@@ -6,11 +6,12 @@ import {EntryItem} from "./EntryListItem/EntryListItem";
import down from "./assets/downImg.svg";
import spinner from './assets/spinner.svg';
import Api from "../helpers/api";
import {useRecoilState, useRecoilValue} from "recoil";
import entriesAtom from "../recoil/entries";
import wsConnectionAtom, {WsConnectionStatus} from "../recoil/wsConnection";
import queryAtom from "../recoil/query";
interface EntriesListProps {
entries: any[];
setEntries: any;
query: string;
listEntryREF: any;
onSnapBrokenEvent: () => void;
isSnappedToBottom: boolean;
@@ -22,12 +23,8 @@ interface EntriesListProps {
startTime: number;
noMoreDataTop: boolean;
setNoMoreDataTop: (flag: boolean) => void;
focusedEntryId: string;
setFocusedEntryId: (id: string) => void;
updateQuery: any;
leftOffTop: number;
setLeftOffTop: (leftOffTop: number) => void;
isWebSocketConnectionClosed: boolean;
ws: any;
openWebSocket: (query: string, resetEntries: boolean) => void;
leftOffBottom: number;
@@ -38,7 +35,13 @@ interface EntriesListProps {
const api = Api.getInstance();
export const EntriesList: React.FC<EntriesListProps> = ({entries, setEntries, query, listEntryREF, onSnapBrokenEvent, isSnappedToBottom, setIsSnappedToBottom, queriedCurrent, setQueriedCurrent, queriedTotal, setQueriedTotal, startTime, noMoreDataTop, setNoMoreDataTop, focusedEntryId, setFocusedEntryId, updateQuery, leftOffTop, setLeftOffTop, isWebSocketConnectionClosed, ws, openWebSocket, leftOffBottom, truncatedTimestamp, setTruncatedTimestamp, scrollableRef}) => {
export const EntriesList: React.FC<EntriesListProps> = ({listEntryREF, onSnapBrokenEvent, isSnappedToBottom, setIsSnappedToBottom, queriedCurrent, setQueriedCurrent, queriedTotal, setQueriedTotal, startTime, noMoreDataTop, setNoMoreDataTop, leftOffTop, setLeftOffTop, ws, openWebSocket, leftOffBottom, truncatedTimestamp, setTruncatedTimestamp, scrollableRef}) => {
const [entries, setEntries] = useRecoilState(entriesAtom);
const wsConnection = useRecoilValue(wsConnectionAtom);
const query = useRecoilValue(queryAtom);
const isWsConnectionClosed = wsConnection === WsConnectionStatus.Closed;
const [loadMoreTop, setLoadMoreTop] = useState(false);
const [isLoadingTop, setIsLoadingTop] = useState(false);
@@ -95,9 +98,9 @@ export const EntriesList: React.FC<EntriesListProps> = ({entries, setEntries, qu
},[setLoadMoreTop, setIsLoadingTop, entries, setEntries, query, setNoMoreDataTop, leftOffTop, setLeftOffTop, queriedCurrent, setQueriedCurrent, setQueriedTotal, setTruncatedTimestamp, scrollableRef]);
useEffect(() => {
if(!isWebSocketConnectionClosed || !loadMoreTop || noMoreDataTop) return;
if(!isWsConnectionClosed || !loadMoreTop || noMoreDataTop) return;
getOldEntries();
}, [loadMoreTop, noMoreDataTop, getOldEntries, isWebSocketConnectionClosed]);
}, [loadMoreTop, noMoreDataTop, getOldEntries, isWsConnectionClosed]);
const scrollbarVisible = scrollableRef.current?.childWrapperRef.current.clientHeight > scrollableRef.current?.wrapperRef.current.clientHeight;
@@ -113,10 +116,7 @@ export const EntriesList: React.FC<EntriesListProps> = ({entries, setEntries, qu
{memoizedEntries.map(entry => <EntryItem
key={`entry-${entry.id}`}
entry={entry}
focusedEntryId={focusedEntryId}
setFocusedEntryId={setFocusedEntryId}
style={{}}
updateQuery={updateQuery}
headingMode={false}
/>)}
</ScrollableFeedVirtualized>
@@ -131,9 +131,9 @@ export const EntriesList: React.FC<EntriesListProps> = ({entries, setEntries, qu
</button>
<button type="button"
title="Snap to bottom"
className={`${styles.btnLive} ${isSnappedToBottom && !isWebSocketConnectionClosed ? styles.hideButton : styles.showButton}`}
className={`${styles.btnLive} ${isSnappedToBottom && !isWsConnectionClosed ? styles.hideButton : styles.showButton}`}
onClick={(_) => {
if (isWebSocketConnectionClosed) {
if (isWsConnectionClosed) {
if (query) {
openWebSocket(`(${query}) and leftOff(${leftOffBottom})`, false);
} else {
@@ -148,7 +148,7 @@ export const EntriesList: React.FC<EntriesListProps> = ({entries, setEntries, qu
</div>
<div className={styles.footer}>
<div>Displaying <b>{entries?.length}</b> results out of <b>{queriedTotal}</b> total</div>
<div>Displaying <b id="entries-length">{entries?.length}</b> results out of <b id="total-entries">{queriedTotal}</b> total</div>
{startTime !== 0 && <div>Started listening at <span style={{marginRight: 5, fontWeight: 600, fontSize: 13}}>{Moment(truncatedTimestamp ? truncatedTimestamp : startTime).utc().format('MM/DD/YYYY, h:mm:ss.SSS A')}</span></div>}
</div>
</div>

View File

@@ -1,9 +1,13 @@
import React from "react";
import React, {useEffect, useState} from "react";
import EntryViewer from "./EntryDetailed/EntryViewer";
import {EntryItem} from "./EntryListItem/EntryListItem";
import {makeStyles} from "@material-ui/core";
import Protocol from "./UI/Protocol"
import Queryable from "./UI/Queryable";
import {toast} from "react-toastify";
import {useRecoilValue} from "recoil";
import focusedEntryIdAtom from "../recoil/focusedEntryId";
import Api from "../helpers/api";
const useStyles = makeStyles(() => ({
entryTitle: {
@@ -27,23 +31,17 @@ const useStyles = makeStyles(() => ({
}
}));
interface EntryDetailedProps {
entryData: any
updateQuery: any
}
export const formatSize = (n: number) => n > 1000 ? `${Math.round(n / 1000)}KB` : `${n} B`;
const EntryTitle: React.FC<any> = ({protocol, data, bodySize, elapsedTime, updateQuery}) => {
const EntryTitle: React.FC<any> = ({protocol, data, bodySize, elapsedTime}) => {
const classes = useStyles();
const response = data.response;
return <div className={classes.entryTitle}>
<Protocol protocol={protocol} horizontal={true} updateQuery={updateQuery}/>
<Protocol protocol={protocol} horizontal={true}/>
<div style={{right: "30px", position: "absolute", display: "flex"}}>
{response && <Queryable
query={`response.bodySize == ${bodySize}`}
updateQuery={updateQuery}
style={{margin: "0 18px"}}
displayIconOnMouseOver={true}
>
@@ -55,7 +53,6 @@ const EntryTitle: React.FC<any> = ({protocol, data, bodySize, elapsedTime, updat
</Queryable>}
{response && <Queryable
query={`elapsedTime >= ${elapsedTime}`}
updateQuery={updateQuery}
style={{marginRight: 18}}
displayIconOnMouseOver={true}
>
@@ -69,30 +66,58 @@ const EntryTitle: React.FC<any> = ({protocol, data, bodySize, elapsedTime, updat
</div>;
};
const EntrySummary: React.FC<any> = ({entry, updateQuery}) => {
const EntrySummary: React.FC<any> = ({entry}) => {
return <EntryItem
key={`entry-${entry.id}`}
entry={entry}
focusedEntryId={null}
setFocusedEntryId={null}
style={{}}
updateQuery={updateQuery}
headingMode={true}
/>;
};
export const EntryDetailed: React.FC<EntryDetailedProps> = ({entryData, updateQuery}) => {
const api = Api.getInstance();
export const EntryDetailed = () => {
const focusedEntryId = useRecoilValue(focusedEntryIdAtom);
const [entryData, setEntryData] = useState(null);
useEffect(() => {
if (!focusedEntryId) return;
setEntryData(null);
(async () => {
try {
const entryData = await api.getEntry(focusedEntryId);
setEntryData(entryData);
} catch (error) {
if (error.response?.data?.type) {
toast[error.response.data.type](`Entry[${focusedEntryId}]: ${error.response.data.msg}`, {
position: "bottom-right",
theme: "colored",
autoClose: error.response.data.autoClose,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
});
}
console.error(error);
}
})();
// eslint-disable-next-line
}, [focusedEntryId]);
return <>
<EntryTitle
{entryData && <EntryTitle
protocol={entryData.protocol}
data={entryData.data}
bodySize={entryData.bodySize}
elapsedTime={entryData.data.elapsedTime}
updateQuery={updateQuery}
/>
{entryData.data && <EntrySummary entry={entryData.data} updateQuery={updateQuery}/>}
/>}
{entryData && <EntrySummary entry={entryData.data}/>}
<>
{entryData.data && <EntryViewer
{entryData && <EntryViewer
representation={entryData.representation}
isRulesEnabled={entryData.isRulesEnabled}
rulesMatched={entryData.rulesMatched}
@@ -102,7 +127,6 @@ export const EntryDetailed: React.FC<EntryDetailedProps> = ({entryData, updateQu
contractContent={entryData.data.contractContent}
elapsedTime={entryData.data.elapsedTime}
color={entryData.protocol.backgroundColor}
updateQuery={updateQuery}
/>}
</>
</>

View File

@@ -12,14 +12,13 @@ import {default as xmlBeautify} from "xml-formatter";
interface EntryViewLineProps {
label: string;
value: number | string;
updateQuery?: any;
selector?: string;
overrideQueryValue?: string;
displayIconOnMouseOver?: boolean;
useTooltip?: boolean;
}
const EntryViewLine: React.FC<EntryViewLineProps> = ({label, value, updateQuery = null, selector = "", overrideQueryValue = "", displayIconOnMouseOver = true, useTooltip = true}) => {
const EntryViewLine: React.FC<EntryViewLineProps> = ({label, value, selector = "", overrideQueryValue = "", displayIconOnMouseOver = true, useTooltip = true}) => {
let query: string;
if (!selector) {
query = "";
@@ -34,7 +33,6 @@ const EntryViewLine: React.FC<EntryViewLineProps> = ({label, value, updateQuery
<td className={`${styles.dataKey}`}>
<Queryable
query={query}
updateQuery={updateQuery}
style={{float: "right", height: "18px"}}
iconStyle={{marginRight: "20px"}}
flipped={true}
@@ -63,10 +61,9 @@ interface EntrySectionCollapsibleTitleProps {
expanded: boolean,
setExpanded: any,
query?: string,
updateQuery?: any,
}
const EntrySectionCollapsibleTitle: React.FC<EntrySectionCollapsibleTitleProps> = ({title, color, expanded, setExpanded, query = "", updateQuery = null}) => {
const EntrySectionCollapsibleTitle: React.FC<EntrySectionCollapsibleTitleProps> = ({title, color, expanded, setExpanded, query = ""}) => {
return <div className={styles.title}>
<div
className={`${styles.button} ${expanded ? styles.expanded : ''}`}
@@ -79,9 +76,8 @@ const EntrySectionCollapsibleTitle: React.FC<EntrySectionCollapsibleTitleProps>
</div>
<Queryable
query={query}
updateQuery={updateQuery}
useTooltip={updateQuery ? true : false}
displayIconOnMouseOver={updateQuery ? true : false}
useTooltip={!!query}
displayIconOnMouseOver={!!query}
>
<span>{title}</span>
</Queryable>
@@ -92,15 +88,14 @@ interface EntrySectionContainerProps {
title: string,
color: string,
query?: string,
updateQuery?: any,
}
export const EntrySectionContainer: React.FC<EntrySectionContainerProps> = ({title, color, children, query = "", updateQuery = null}) => {
export const EntrySectionContainer: React.FC<EntrySectionContainerProps> = ({title, color, children, query = ""}) => {
const [expanded, setExpanded] = useState(true);
return <CollapsibleContainer
className={styles.collapsibleContainer}
expanded={expanded}
title={<EntrySectionCollapsibleTitle title={title} color={color} expanded={expanded} setExpanded={setExpanded} query={query} updateQuery={updateQuery}/>}
title={<EntrySectionCollapsibleTitle title={title} color={color} expanded={expanded} setExpanded={setExpanded} query={query}/>}
>
{children}
</CollapsibleContainer>
@@ -110,7 +105,6 @@ interface EntryBodySectionProps {
title: string,
content: any,
color: string,
updateQuery: any,
encoding?: string,
contentType?: string,
selector?: string,
@@ -119,7 +113,6 @@ interface EntryBodySectionProps {
export const EntryBodySection: React.FC<EntryBodySectionProps> = ({
title,
color,
updateQuery,
content,
encoding,
contentType,
@@ -172,7 +165,6 @@ export const EntryBodySection: React.FC<EntryBodySectionProps> = ({
title={title}
color={color}
query={`${selector} == r".*"`}
updateQuery={updateQuery}
>
<div style={{display: 'flex', alignItems: 'center', alignContent: 'center', margin: "5px 0"}}>
{supportsPrettying && <div style={{paddingTop: 3}}>
@@ -203,10 +195,9 @@ interface EntrySectionProps {
title: string,
color: string,
arrayToIterate: any[],
updateQuery: any,
}
export const EntryTableSection: React.FC<EntrySectionProps> = ({title, color, arrayToIterate, updateQuery}) => {
export const EntryTableSection: React.FC<EntrySectionProps> = ({title, color, arrayToIterate}) => {
let arrayToIterateSorted: any[];
if (arrayToIterate) {
arrayToIterateSorted = arrayToIterate.sort((a, b) => {
@@ -226,12 +217,11 @@ export const EntryTableSection: React.FC<EntrySectionProps> = ({title, color, ar
arrayToIterate && arrayToIterate.length > 0 ?
<EntrySectionContainer title={title} color={color}>
<table>
<tbody>
<tbody id={`tbody-${title}`}>
{arrayToIterateSorted.map(({name, value, selector}, index) => <EntryViewLine
key={index}
label={name}
value={value}
updateQuery={updateQuery}
selector={selector}
/>)}
</tbody>

View File

@@ -8,7 +8,7 @@ enum SectionTypes {
SectionBody = "body",
}
const SectionsRepresentation: React.FC<any> = ({data, color, updateQuery}) => {
const SectionsRepresentation: React.FC<any> = ({data, color}) => {
const sections = []
if (data) {
@@ -16,12 +16,12 @@ const SectionsRepresentation: React.FC<any> = ({data, color, updateQuery}) => {
switch (row.type) {
case SectionTypes.SectionTable:
sections.push(
<EntryTableSection key={i} title={row.title} color={color} arrayToIterate={JSON.parse(row.data)} updateQuery={updateQuery}/>
<EntryTableSection key={i} title={row.title} color={color} arrayToIterate={JSON.parse(row.data)}/>
)
break;
case SectionTypes.SectionBody:
sections.push(
<EntryBodySection key={i} title={row.title} color={color} content={row.data} updateQuery={updateQuery} encoding={row.encoding} contentType={row.mimeType} selector={row.selector}/>
<EntryBodySection key={i} title={row.title} color={color} content={row.data} encoding={row.encoding} contentType={row.mimeType} selector={row.selector}/>
)
break;
default:
@@ -33,7 +33,7 @@ const SectionsRepresentation: React.FC<any> = ({data, color, updateQuery}) => {
return <>{sections}</>;
}
const AutoRepresentation: React.FC<any> = ({representation, isRulesEnabled, rulesMatched, contractStatus, requestReason, responseReason, contractContent, elapsedTime, color, updateQuery}) => {
const AutoRepresentation: React.FC<any> = ({representation, isRulesEnabled, rulesMatched, contractStatus, requestReason, responseReason, contractContent, elapsedTime, color}) => {
var TABS = [
{
tab: 'Request'
@@ -48,9 +48,9 @@ const AutoRepresentation: React.FC<any> = ({representation, isRulesEnabled, rule
const {request, response} = JSON.parse(representation);
var responseTabIndex = 0;
var rulesTabIndex = 0;
var contractTabIndex = 0;
let responseTabIndex = 0;
let rulesTabIndex = 0;
let contractTabIndex = 0;
if (response) {
TABS.push(
@@ -85,10 +85,10 @@ const AutoRepresentation: React.FC<any> = ({representation, isRulesEnabled, rule
<Tabs tabs={TABS} currentTab={currentTab} color={color} onChange={setCurrentTab} leftAligned/>
</div>
{currentTab === TABS[0].tab && <React.Fragment>
<SectionsRepresentation data={request} color={color} updateQuery={updateQuery}/>
<SectionsRepresentation data={request} color={color}/>
</React.Fragment>}
{response && currentTab === TABS[responseTabIndex].tab && <React.Fragment>
<SectionsRepresentation data={response} color={color} updateQuery={updateQuery}/>
<SectionsRepresentation data={response} color={color}/>
</React.Fragment>}
{isRulesEnabled && currentTab === TABS[rulesTabIndex].tab && <React.Fragment>
<EntryTablePolicySection title={'Rule'} color={color} latency={elapsedTime} arrayToIterate={rulesMatched ? rulesMatched : []}/>
@@ -110,10 +110,9 @@ interface Props {
contractContent: string;
color: string;
elapsedTime: number;
updateQuery: any;
}
const EntryViewer: React.FC<Props> = ({representation, isRulesEnabled, rulesMatched, contractStatus, requestReason, responseReason, contractContent, elapsedTime, color, updateQuery}) => {
const EntryViewer: React.FC<Props> = ({representation, isRulesEnabled, rulesMatched, contractStatus, requestReason, responseReason, contractContent, elapsedTime, color}) => {
return <AutoRepresentation
representation={representation}
isRulesEnabled={isRulesEnabled}
@@ -124,7 +123,6 @@ const EntryViewer: React.FC<Props> = ({representation, isRulesEnabled, rulesMatc
contractContent={contractContent}
elapsedTime={elapsedTime}
color={color}
updateQuery={updateQuery}
/>
};

View File

@@ -74,7 +74,6 @@
.separatorRight
display: flex
border-right: 1px solid $data-background-color
padding: 4px
padding-right: 12px
.separatorLeft

View File

@@ -12,6 +12,9 @@ import ingoingIconNeutral from "../assets/ingoing-traffic-neutral.svg"
import outgoingIconSuccess from "../assets/outgoing-traffic-success.svg"
import outgoingIconFailure from "../assets/outgoing-traffic-failure.svg"
import outgoingIconNeutral from "../assets/outgoing-traffic-neutral.svg"
import {useRecoilState} from "recoil";
import focusedEntryIdAtom from "../../recoil/focusedEntryId";
import queryAtom from "../../recoil/query";
interface TCPInterface {
ip: string
@@ -42,15 +45,14 @@ interface Rules {
interface EntryProps {
entry: Entry;
focusedEntryId: string;
setFocusedEntryId: (id: string) => void;
style: object;
updateQuery: any;
headingMode: boolean;
}
export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocusedEntryId, style, updateQuery, headingMode}) => {
export const EntryItem: React.FC<EntryProps> = ({entry, style, headingMode}) => {
const [focusedEntryId, setFocusedEntryId] = useRecoilState(focusedEntryIdAtom);
const [queryState, setQuery] = useRecoilState(queryAtom);
const isSelected = focusedEntryId === entry.id.toString();
const classification = getClassification(entry.status)
@@ -103,8 +105,8 @@ export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocus
}
}
var contractEnabled = true;
var contractText = "";
let contractEnabled = true;
let contractText = "";
switch (entry.contractStatus) {
case 0:
contractEnabled = false;
@@ -123,8 +125,9 @@ export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocus
break;
}
const isStatusCodeEnabled = ((entry.proto.name === "http" && "status" in entry) || entry.status !== 0);
var endpointServiceContainer = "10px";
let endpointServiceContainer = "10px";
if (!isStatusCodeEnabled) endpointServiceContainer = "20px";
return <>
@@ -137,7 +140,7 @@ export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocus
setFocusedEntryId(entry.id.toString());
}}
style={{
border: isSelected ? `1px ${entry.proto.backgroundColor} solid` : "1px transparent solid",
border: isSelected && !headingMode ? `1px ${entry.proto.backgroundColor} solid` : "1px transparent solid",
position: !headingMode ? "absolute" : "unset",
top: style['top'],
marginTop: !headingMode ? style['marginTop'] : "10px",
@@ -147,17 +150,15 @@ export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocus
{!headingMode ? <Protocol
protocol={entry.proto}
horizontal={false}
updateQuery={updateQuery}
/> : null}
{isStatusCodeEnabled && <div>
<StatusCode statusCode={entry.status} updateQuery={updateQuery}/>
<StatusCode statusCode={entry.status}/>
</div>}
<div className={styles.endpointServiceContainer} style={{paddingLeft: endpointServiceContainer}}>
<Summary method={entry.method} summary={entry.summary} updateQuery={updateQuery}/>
<Summary method={entry.method} summary={entry.summary}/>
<div className={styles.resolvedName}>
<Queryable
query={`src.name == "${entry.src.name}"`}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
flipped={true}
style={{marginTop: "-4px", overflow: "visible"}}
@@ -174,7 +175,6 @@ export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocus
<SwapHorizIcon style={{color: entry.proto.backgroundColor, marginTop: "-2px"}}></SwapHorizIcon>
<Queryable
query={`dst.name == "${entry.dst.name}"`}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
style={{marginTop: "-4px"}}
iconStyle={{marginTop: "4px", marginLeft: "-2px"}}
@@ -204,7 +204,6 @@ export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocus
<div className={styles.separatorRight}>
<Queryable
query={`src.ip == "${entry.src.ip}"`}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
flipped={true}
iconStyle={{marginRight: "16px"}}
@@ -219,7 +218,6 @@ export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocus
<span className={`${styles.tcpInfo}`} style={{marginTop: "18px"}}>{entry.src.port ? ":" : ""}</span>
<Queryable
query={`src.port == "${entry.src.port}"`}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
flipped={true}
iconStyle={{marginTop: "28px"}}
@@ -234,7 +232,6 @@ export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocus
{entry.isOutgoing ?
<Queryable
query={`outgoing == true`}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
flipped={true}
iconStyle={{marginTop: "28px"}}
@@ -248,7 +245,6 @@ export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocus
:
<Queryable
query={`outgoing == true`}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
flipped={true}
iconStyle={{marginTop: "28px"}}
@@ -258,14 +254,14 @@ export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocus
alt="Outgoing traffic"
title="Outgoing"
onClick={() => {
updateQuery(`outgoing == false`)
const query = `outgoing == false`;
setQuery(queryState ? `${queryState} and ${query}` : query);
}}
/>
</Queryable>
}
<Queryable
query={`dst.ip == "${entry.dst.ip}"`}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
flipped={false}
iconStyle={{marginTop: "28px"}}
@@ -280,7 +276,6 @@ export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocus
<span className={`${styles.tcpInfo}`} style={{marginTop: "18px"}}>:</span>
<Queryable
query={`dst.port == "${entry.dst.port}"`}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
flipped={false}
>
@@ -295,7 +290,6 @@ export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocus
<div className={styles.timestamp}>
<Queryable
query={`timestamp >= datetime("${Moment(+entry.timestamp)?.utc().format('MM/DD/YYYY, h:mm:ss.SSS A')}")`}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
flipped={false}
>

View File

@@ -7,20 +7,20 @@ import {SyntaxHighlighter} from "./UI/SyntaxHighlighter/index";
import filterUIExample1 from "./assets/filter-ui-example-1.png"
import filterUIExample2 from "./assets/filter-ui-example-2.png"
import variables from '../variables.module.scss';
import {useRecoilState} from "recoil";
import queryAtom from "../recoil/query";
import useKeyPress from "../hooks/useKeyPress"
import shortcutsKeyboard from "../configs/shortcutsKeyboard"
interface FiltersProps {
query: string
setQuery: any
backgroundColor: string
ws: any
openWebSocket: (query: string, resetEntries: boolean) => void;
}
export const Filters: React.FC<FiltersProps> = ({query, setQuery, backgroundColor, ws, openWebSocket}) => {
export const Filters: React.FC<FiltersProps> = ({backgroundColor, ws, openWebSocket}) => {
return <div className={styles.container}>
<QueryForm
query={query}
setQuery={setQuery}
backgroundColor={backgroundColor}
ws={ws}
openWebSocket={openWebSocket}
@@ -29,8 +29,6 @@ export const Filters: React.FC<FiltersProps> = ({query, setQuery, backgroundColo
};
interface QueryFormProps {
query: string
setQuery: any
backgroundColor: string
ws: any
openWebSocket: (query: string, resetEntries: boolean) => void;
@@ -50,9 +48,10 @@ export const modalStyle = {
color: '#000',
};
export const QueryForm: React.FC<QueryFormProps> = ({query, setQuery, backgroundColor, ws, openWebSocket}) => {
export const QueryForm: React.FC<QueryFormProps> = ({backgroundColor, ws, openWebSocket}) => {
const formRef = useRef<HTMLFormElement>(null);
const [query, setQuery] = useRecoilState(queryAtom);
const [openModal, setOpenModal] = useState(false);
@@ -63,6 +62,8 @@ export const QueryForm: React.FC<QueryFormProps> = ({query, setQuery, background
setQuery(e.target.value);
}
const handleSubmit = (e) => {
ws.close();
if (query) {
@@ -73,6 +74,8 @@ export const QueryForm: React.FC<QueryFormProps> = ({query, setQuery, background
e.preventDefault();
}
useKeyPress(shortcutsKeyboard.ctrlEnter, handleSubmit, formRef.current);
return <>
<form
ref={formRef}

View File

@@ -1,4 +1,4 @@
import React, {useContext, useEffect, useState} from "react";
import React, {useEffect, useState} from "react";
import logo from '../assets/MizuEntLogo.svg';
import './Header.sass';
import userImg from '../assets/user-circle.svg';
@@ -9,7 +9,8 @@ import logoutIcon from '../assets/logout.png';
import {SettingsModal} from "../SettingsModal/SettingModal";
import Api from "../../helpers/api";
import {toast} from "react-toastify";
import {MizuContext, Page} from "../../EntApp";
import {useSetRecoilState} from "recoil";
import entPageAtom, {Page} from "../../recoil/entPage";
const api = Api.getInstance();
@@ -49,12 +50,12 @@ export const EntHeader: React.FC<EntHeaderProps> = ({isFirstLogin, setIsFirstLog
const ProfileButton = () => {
const {setPage} = useContext(MizuContext);
const setEntPage = useSetRecoilState(entPageAtom);
const logout = async (popupState) => {
try {
await api.logout();
setPage(Page.Login);
setEntPage(Page.Login);
} catch (e) {
toast.error("Something went wrong, please check the console");
console.error(e);

View File

@@ -1,11 +1,15 @@
import { Button } from "@material-ui/core";
import React, { useContext, useState } from "react";
import { MizuContext, Page } from "../EntApp";
import React, { useState,useRef } from "react";
import { adminUsername } from "../consts";
import Api, { FormValidationErrorType } from "../helpers/api";
import { toast } from 'react-toastify';
import LoadingOverlay from "./LoadingOverlay";
import { useCommonStyles } from "../helpers/commonStyle";
import {useSetRecoilState} from "recoil";
import entPageAtom, {Page} from "../recoil/entPage";
import useKeyPress from "../hooks/useKeyPress"
import shortcutsKeyboard from "../configs/shortcutsKeyboard"
const api = Api.getInstance();
@@ -15,12 +19,13 @@ interface InstallPageProps {
export const InstallPage: React.FC<InstallPageProps> = ({onFirstLogin}) => {
const formRef = useRef(null);
const classes = useCommonStyles();
const [isLoading, setIsLoading] = useState(false);
const [password, setPassword] = useState("");
const [passwordConfirm, setPasswordConfirm] = useState("");
const {setPage} = useContext(MizuContext);
const setEntPage = useSetRecoilState(entPageAtom);
const onFormSubmit = async () => {
if (password.length < 4) {
@@ -35,7 +40,7 @@ export const InstallPage: React.FC<InstallPageProps> = ({onFirstLogin}) => {
setIsLoading(true);
await api.register(adminUsername, password);
if (!await api.isAuthenticationNeeded()) {
setPage(Page.Traffic);
setEntPage(Page.Traffic);
onFirstLogin();
}
} catch (e) {
@@ -53,13 +58,9 @@ export const InstallPage: React.FC<InstallPageProps> = ({onFirstLogin}) => {
}
const handleFormOnKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
onFormSubmit();
}
};
useKeyPress(shortcutsKeyboard.enter, onFormSubmit, formRef.current);
return <div className="centeredForm" onKeyPress={handleFormOnKeyPress}>
return <div className="centeredForm" ref={formRef}>
{isLoading && <LoadingOverlay/>}
<div className="form-title left-text">Setup</div>
<span className="form-subtitle">Welcome to Mizu, please set up the admin user to continue</span>

View File

@@ -1,10 +1,14 @@
import { Button } from "@material-ui/core";
import React, { useContext, useState } from "react";
import React, { useState,useRef } from "react";
import { toast } from "react-toastify";
import { MizuContext, Page } from "../EntApp";
import Api from "../helpers/api";
import { useCommonStyles } from "../helpers/commonStyle";
import LoadingOverlay from "./LoadingOverlay";
import entPageAtom, {Page} from "../recoil/entPage";
import {useSetRecoilState} from "recoil";
import useKeyPress from "../hooks/useKeyPress"
import shortcutsKeyboard from "../configs/shortcutsKeyboard"
const api = Api.getInstance();
@@ -14,8 +18,9 @@ const LoginPage: React.FC = () => {
const [isLoading, setIsLoading] = useState(false);
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const formRef = useRef(null);
const {setPage} = useContext(MizuContext);
const setEntPage = useSetRecoilState(entPageAtom);
const onFormSubmit = async () => {
setIsLoading(true);
@@ -23,7 +28,7 @@ const LoginPage: React.FC = () => {
try {
await api.login(username, password);
if (!await api.isAuthenticationNeeded()) {
setPage(Page.Traffic);
setEntPage(Page.Traffic);
} else {
toast.error("Invalid credentials");
}
@@ -35,13 +40,9 @@ const LoginPage: React.FC = () => {
}
}
const handleFormOnKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
onFormSubmit();
}
};
useKeyPress(shortcutsKeyboard.enter, onFormSubmit, formRef.current);
return <div className="centeredForm" onKeyPress={handleFormOnKeyPress}>
return <div className="centeredForm" ref={formRef}>
{isLoading && <LoadingOverlay/>}
<div className="form-title left-text">Login</div>
<div className="form-input">

View File

@@ -0,0 +1,6 @@
@import '../../variables.module.scss'
.NotSelectedMessage
margin-left: 41%
padding-top: 3%
font-size: large

View File

@@ -0,0 +1,95 @@
import { Box, Fade, FormControl, MenuItem, Modal } from "@material-ui/core";
import { useEffect, useState } from "react";
import { RedocStandalone } from "redoc";
import Api from "../../helpers/api";
import { Select } from "../UI/Select";
import closeIcon from "../assets/closeIcon.svg";
import { toast } from 'react-toastify';
import './OasModal.sass'
const api = Api.getInstance();
const noOasServiceSelectedMessage = "Please Select OasService";
const OasModal = ({ openModal, handleCloseModal }) => {
const [oasServices, setOasServices] = useState([])
const [selectedServiceName, setSelectedServiceName] = useState("");
const [selectedServiceSpec, setSelectedServiceSpec] = useState(null);
useEffect(() => {
(async () => {
try {
const services = await api.getOasServices();
setOasServices(services);
} catch (e) {
toast.error("Error occurred while fetching services list");
console.error(e);
}
})();
}, [openModal]);
const onSelectedOASService = async (selectedService) => {
setSelectedServiceName(selectedService);
if(oasServices.length === 0){
return
}
try {
const data = await api.getOasByService(selectedService);
setSelectedServiceSpec(data);
} catch (e) {
toast.error("Error occurred while fetching service OAS spec");
console.error(e);
}
};
return (
<Modal
aria-labelledby="transition-modal-title"
aria-describedby="transition-modal-description"
open={openModal}
onClose={handleCloseModal}
closeAfterTransition
hideBackdrop={true}
style={{ overflow: "auto", backgroundColor: "#ffffff", color:"black" }}
>
<Fade in={openModal}>
<Box>
<div
style={{
display: "flex",
justifyContent: "space-between",
padding: "1%",
}}
>
<div style={{ marginLeft: "40%" }}>
<FormControl>
<Select
labelId="service-select-label"
id="service-select"
label="Show OAS"
placeholder="Show OAS"
value={selectedServiceName}
onChange={onSelectedOASService}
>
{oasServices.map((service) => (
<MenuItem key={service} value={service}>
{service}
</MenuItem>
))}
</Select>
</FormControl>
</div>
<div style={{ cursor: "pointer" }}>
<img src={closeIcon} alt="Back" onClick={handleCloseModal} />
</div>
</div>
{selectedServiceSpec && <RedocStandalone spec={selectedServiceSpec} />}
<div className="NotSelectedMessage">
{!selectedServiceName && noOasServiceSelectedMessage}
</div>
</Box>
</Fade>
</Modal>
);
};
export default OasModal;

View File

@@ -0,0 +1,216 @@
import React, { useState, useEffect, useCallback } from "react";
import { Box, Fade, Modal, Backdrop, Button } from "@material-ui/core";
import { toast } from "react-toastify";
import Api from "../../helpers/api";
import spinnerStyle from '../style/Spinner.module.sass';
import spinnerImg from '../assets/spinner.svg';
import Graph from "react-graph-vis";
import debounce from 'lodash/debounce';
import ServiceMapOptions from './ServiceMapOptions'
import { useCommonStyles } from "../../helpers/commonStyle";
interface GraphData {
nodes: Node[];
edges: Edge[];
}
interface Node {
id: number;
value: number;
label: string;
title?: string;
color?: object;
}
interface Edge {
from: number;
to: number;
value: number;
label: string;
title?: string;
color?: object;
}
interface ServiceMapNode {
id: number;
name: string;
entry: Entry;
count: number;
}
interface ServiceMapEdge {
source: ServiceMapNode;
destination: ServiceMapNode;
count: number;
protocol: Protocol;
}
interface ServiceMapGraph {
nodes: ServiceMapNode[];
edges: ServiceMapEdge[];
}
interface Entry {
ip: string;
port: string;
name: string;
}
interface Protocol {
name: string;
abbr: string;
macro: string;
version: string;
backgroundColor: string;
foregroundColor: string;
fontSize: number;
referenceLink: string;
ports: string[];
priority: number;
}
interface ServiceMapModalProps {
isOpen: boolean;
onOpen: () => void;
onClose: () => void;
}
const modalStyle = {
position: 'absolute',
top: '10%',
left: '50%',
transform: 'translate(-50%, 0%)',
width: '80vw',
height: '80vh',
bgcolor: 'background.paper',
borderRadius: '5px',
boxShadow: 24,
p: 4,
color: '#000',
};
const api = Api.getInstance();
export const ServiceMapModal: React.FC<ServiceMapModalProps> = ({ isOpen, onOpen, onClose }) => {
const commonClasses = useCommonStyles();
const [isLoading, setIsLoading] = useState<boolean>(true);
const [graphData, setGraphData] = useState<GraphData>({ nodes: [], edges: [] });
const getServiceMapData = useCallback(async () => {
try {
setIsLoading(true)
const serviceMapData: ServiceMapGraph = await api.serviceMapData()
const newGraphData: GraphData = { nodes: [], edges: [] }
if (serviceMapData.nodes) {
newGraphData.nodes = serviceMapData.nodes.map(node => {
return {
id: node.id,
value: node.count,
label: (node.entry.name === "unresolved") ? node.name : `${node.entry.name} (${node.name})`,
title: "Count: " + node.name,
}
})
}
if (serviceMapData.edges) {
newGraphData.edges = serviceMapData.edges.map(edge => {
return {
from: edge.source.id,
to: edge.destination.id,
value: edge.count,
label: edge.count.toString(),
color: {
color: edge.protocol.backgroundColor,
highlight: edge.protocol.backgroundColor
},
}
})
}
setGraphData(newGraphData)
} catch (ex) {
toast.error("An error occurred while loading Mizu Service Map, see console for mode details");
console.error(ex);
} finally {
setIsLoading(false)
}
// eslint-disable-next-line
}, [isOpen])
useEffect(() => {
getServiceMapData()
}, [getServiceMapData])
const resetServiceMap = debounce(async () => {
try {
const serviceMapResetResponse = await api.serviceMapReset();
if (serviceMapResetResponse["status"] === "enabled") {
refreshServiceMap()
}
} catch (ex) {
toast.error("An error occurred while resetting Mizu Service Map, see console for mode details");
console.error(ex);
}
}, 500);
const refreshServiceMap = debounce(() => {
getServiceMapData();
}, 500);
return (
<Modal
aria-labelledby="transition-modal-title"
aria-describedby="transition-modal-description"
open={isOpen}
onClose={onClose}
closeAfterTransition
BackdropComponent={Backdrop}
BackdropProps={{
timeout: 500,
}}
style={{ overflow: 'auto' }}
>
<Fade in={isOpen}>
<Box sx={modalStyle}>
{isLoading && <div className={spinnerStyle.spinnerContainer}>
<img alt="spinner" src={spinnerImg} style={{ height: 50 }} />
</div>}
{!isLoading && <div style={{ height: "100%", width: "100%" }}>
<Button
variant="contained"
className={commonClasses.button}
style={{ marginRight: 25 }}
onClick={() => onClose()}
>
Close
</Button>
<Button
variant="contained"
className={commonClasses.button}
style={{ marginRight: 25 }}
onClick={resetServiceMap}
>
Reset
</Button>
<Button
variant="contained"
className={commonClasses.button}
onClick={refreshServiceMap}
>
Refresh
</Button>
<Graph
graph={graphData}
options={ServiceMapOptions}
/>
</div>}
</Box>
</Fade>
</Modal>
);
}

View File

@@ -0,0 +1,83 @@
const ServiceMapOptions = {
physics: {
enabled: true,
solver: 'barnesHut',
barnesHut: {
theta: 0.5,
gravitationalConstant: -2000,
centralGravity: 0.3,
springLength: 180,
springConstant: 0.04,
damping: 0.09,
avoidOverlap: 1
},
},
layout: {
hierarchical: false,
randomSeed: 1 // always on node 1
},
nodes: {
shape: 'dot',
chosen: true,
color: {
background: '#27AE60',
border: '#000000',
highlight: {
background: '#27AE60',
border: '#000000',
},
},
font: {
color: '#343434',
size: 14, // px
face: 'arial',
background: 'none',
strokeWidth: 0, // px
strokeColor: '#ffffff',
align: 'center',
multi: false,
},
borderWidth: 1.5,
borderWidthSelected: 2.5,
labelHighlightBold: true,
opacity: 1,
shadow: true,
},
edges: {
chosen: true,
dashes: false,
arrowStrikethrough: false,
arrows: {
to: {
enabled: true,
},
middle: {
enabled: false,
},
from: {
enabled: false,
}
},
smooth: {
enabled: true,
type: 'dynamic',
roundness: 1.0
},
font: {
color: '#343434',
size: 12, // px
face: 'arial',
background: 'none',
strokeWidth: 2, // px
strokeColor: '#ffffff',
align: 'horizontal',
multi: false,
},
labelHighlightBold: true,
selectionWidth: 1,
shadow: true,
},
autoResize: true,
};
export default ServiceMapOptions

View File

@@ -1,7 +1,7 @@
import React, {useEffect, useMemo, useRef, useState} from "react";
import {Filters} from "./Filters";
import {EntriesList} from "./EntriesList";
import {makeStyles} from "@material-ui/core";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { Filters } from "./Filters";
import { EntriesList } from "./EntriesList";
import { makeStyles, Button } from "@material-ui/core";
import "./style/TrafficPage.sass";
import styles from './style/EntriesList.module.sass';
import {EntryDetailed} from "./EntryDetailed";
@@ -12,57 +12,56 @@ import {StatusBar} from "./UI/StatusBar";
import Api, {MizuWebsocketURL} from "../helpers/api";
import { toast } from 'react-toastify';
import debounce from 'lodash/debounce';
import {useRecoilState, useRecoilValue} from "recoil";
import tappingStatusAtom from "../recoil/tappingStatus";
import entriesAtom from "../recoil/entries";
import focusedEntryIdAtom from "../recoil/focusedEntryId";
import websocketConnectionAtom, {WsConnectionStatus} from "../recoil/wsConnection";
import queryAtom from "../recoil/query";
import OasModal from "./OasModal/OasModal";
import {useCommonStyles} from "../helpers/commonStyle"
const useLayoutStyles = makeStyles(() => ({
details: {
flex: "0 0 50%",
width: "45vw",
padding: "12px 24px",
borderRadius: 4,
marginTop: 15,
background: variables.headerBackgroundColor,
},
details: {
flex: "0 0 50%",
width: "45vw",
padding: "12px 24px",
borderRadius: 4,
marginTop: 15,
background: variables.headerBackgroundColor,
},
viewer: {
display: 'flex',
overflowY: 'auto',
height: "calc(100% - 70px)",
padding: 5,
paddingBottom: 0,
overflow: "auto",
}
viewer: {
display: "flex",
overflowY: "auto",
height: "calc(100% - 70px)",
padding: 5,
paddingBottom: 0,
overflow: "auto",
},
}));
enum ConnectionStatus {
Closed,
Connected,
}
interface TrafficPageProps {
onTLSDetected: (destAddress: string) => void;
setAnalyzeStatus?: (status: any) => void;
setAnalyzeStatus?: (status: any) => void;
onTLSDetected: (destAddress: string) => void;
setOpenServiceMapModal?: (open: boolean) => void;
}
const api = Api.getInstance();
export const TrafficPage: React.FC<TrafficPageProps> = ({onTLSDetected, setAnalyzeStatus}) => {
export const TrafficPage: React.FC<TrafficPageProps> = ({setAnalyzeStatus,onTLSDetected, setOpenServiceMapModal}) => {
const commonClasses = useCommonStyles();
const classes = useLayoutStyles();
const [entries, setEntries] = useState([] as any);
const [focusedEntryId, setFocusedEntryId] = useState(null);
const [selectedEntryData, setSelectedEntryData] = useState(null);
const [connection, setConnection] = useState(ConnectionStatus.Closed);
const [tappingStatus, setTappingStatus] = useRecoilState(tappingStatusAtom);
const [entries, setEntries] = useRecoilState(entriesAtom);
const [focusedEntryId, setFocusedEntryId] = useRecoilState(focusedEntryIdAtom);
const [wsConnection, setWsConnection] = useRecoilState(websocketConnectionAtom);
const query = useRecoilValue(queryAtom);
const [noMoreDataTop, setNoMoreDataTop] = useState(false);
const [tappingStatus, setTappingStatus] = useState(null);
const [isSnappedToBottom, setIsSnappedToBottom] = useState(true);
const [query, setQuery] = useState("");
const [queryBackgroundColor, setQueryBackgroundColor] = useState("#f5f5f5");
const [addition, updateQuery] = useState("");
const [queriedCurrent, setQueriedCurrent] = useState(0);
const [queriedTotal, setQueriedTotal] = useState(0);
@@ -71,42 +70,39 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({onTLSDetected, setAnaly
const [truncatedTimestamp, setTruncatedTimestamp] = useState(0);
const [startTime, setStartTime] = useState(0);
const scrollableRef = useRef(null);
const handleQueryChange = useMemo(() => debounce(async (query: string) => {
if (!query) {
setQueryBackgroundColor("#f5f5f5")
} else {
const [openOasModal, setOpenOasModal] = useState(false);
const handleOpenModal = () => setOpenOasModal(true);
const handleCloseModal = () => setOpenOasModal(false);
const handleQueryChange = useMemo(
() =>
debounce(async (query: string) => {
if (!query) {
setQueryBackgroundColor("#f5f5f5");
} else {
const data = await api.validateQuery(query);
if (!data) {
return;
return;
}
if (data.valid) {
setQueryBackgroundColor("#d2fad2");
setQueryBackgroundColor("#d2fad2");
} else {
setQueryBackgroundColor("#fad6dc");
setQueryBackgroundColor("#fad6dc");
}
}
}, 500), []) as (query: string) => void;
}
}, 500),
[]
) as (query: string) => void;
useEffect(() => {
handleQueryChange(query);
}, [query, handleQueryChange]);
useEffect(() => {
if (query) {
setQuery(`${query} and ${addition}`);
} else {
setQuery(addition);
}
// eslint-disable-next-line
}, [addition]);
const ws = useRef(null);
const listEntry = useRef(null);
const openWebSocket = (query: string, resetEntries: boolean) => {
if (resetEntries) {
setFocusedEntryId(null);
@@ -117,11 +113,11 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({onTLSDetected, setAnaly
}
ws.current = new WebSocket(MizuWebsocketURL);
ws.current.onopen = () => {
setConnection(ConnectionStatus.Connected);
setWsConnection(WsConnectionStatus.Connected);
ws.current.send(query);
}
ws.current.onclose = () => {
setConnection(ConnectionStatus.Closed);
setWsConnection(WsConnectionStatus.Closed);
}
ws.current.onerror = (event) => {
console.error("WebSocket error:", event);
@@ -134,131 +130,105 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({onTLSDetected, setAnaly
}
if (ws.current) {
ws.current.onmessage = e => {
if (!e?.data) return;
const message = JSON.parse(e.data);
switch (message.messageType) {
case "entry":
const entry = message.data;
if (!focusedEntryId) setFocusedEntryId(entry.id.toString())
const newEntries = [...entries, entry];
if (newEntries.length === 10001) {
setLeftOffTop(newEntries[0].entry.id);
newEntries.shift();
setNoMoreDataTop(false);
}
setEntries(newEntries);
break
case "status":
setTappingStatus(message.tappingStatus);
break
case "analyzeStatus":
if(setAnalyzeStatus)
setAnalyzeStatus(message.analyzeStatus);
break
case "outboundLink":
onTLSDetected(message.Data.DstIP);
break;
case "toast":
toast[message.data.type](message.data.text, {
position: "bottom-right",
theme: "colored",
autoClose: message.data.autoClose,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
});
break;
case "queryMetadata":
setQueriedCurrent(queriedCurrent + message.data.current);
setQueriedTotal(message.data.total);
setLeftOffBottom(message.data.leftOff);
setTruncatedTimestamp(message.data.truncatedTimestamp);
if (leftOffTop === null) {
setLeftOffTop(message.data.leftOff - 1);
}
break;
case "startTime":
setStartTime(message.data);
break;
default:
console.error(`unsupported websocket message type, Got: ${message.messageType}`)
ws.current.onmessage = (e) => {
if (!e?.data) return;
const message = JSON.parse(e.data);
switch (message.messageType) {
case "entry":
const entry = message.data;
if (!focusedEntryId) setFocusedEntryId(entry.id.toString());
const newEntries = [...entries, entry];
if (newEntries.length === 10001) {
setLeftOffTop(newEntries[0].entry.id);
newEntries.shift();
setNoMoreDataTop(false);
}
setEntries(newEntries);
break;
case "status":
setTappingStatus(message.tappingStatus);
break;
case "analyzeStatus":
setAnalyzeStatus(message.analyzeStatus);
break;
case "outboundLink":
onTLSDetected(message.Data.DstIP);
break;
case "toast":
toast[message.data.type](message.data.text, {
position: "bottom-right",
theme: "colored",
autoClose: message.data.autoClose,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
});
break;
case "queryMetadata":
setQueriedCurrent(queriedCurrent + message.data.current);
setQueriedTotal(message.data.total);
setLeftOffBottom(message.data.leftOff);
setTruncatedTimestamp(message.data.truncatedTimestamp);
if (leftOffTop === null) {
setLeftOffTop(message.data.leftOff - 1);
}
break;
case "startTime":
setStartTime(message.data);
break;
default:
console.error(
`unsupported websocket message type, Got: ${message.messageType}`
);
}
};
}
useEffect(() => {
(async () => {
openWebSocket("leftOff(-1)", true);
try{
const tapStatusResponse = await api.tapStatus();
setTappingStatus(tapStatusResponse);
if(setAnalyzeStatus) {
const analyzeStatusResponse = await api.analyzeStatus();
setAnalyzeStatus(analyzeStatusResponse);
(async () => {
openWebSocket("leftOff(-1)", true);
try{
const tapStatusResponse = await api.tapStatus();
setTappingStatus(tapStatusResponse);
if(setAnalyzeStatus) {
const analyzeStatusResponse = await api.analyzeStatus();
setAnalyzeStatus(analyzeStatusResponse);
}
} catch (error) {
console.error(error);
}
} catch (error) {
console.error(error);
}
})()
// eslint-disable-next-line
}, []);
useEffect(() => {
if (!focusedEntryId) return;
setSelectedEntryData(null);
(async () => {
try {
const entryData = await api.getEntry(focusedEntryId, query);
setSelectedEntryData(entryData);
} catch (error) {
if (error.response?.data?.type) {
toast[error.response.data.type](`Entry[${focusedEntryId}]: ${error.response.data.msg}`, {
position: "bottom-right",
theme: "colored",
autoClose: error.response.data.autoClose,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
});
}
console.error(error);
}
})();
// eslint-disable-next-line
}, [focusedEntryId]);
})()
// eslint-disable-next-line
}, []);
const toggleConnection = () => {
ws.current.close();
if (connection !== ConnectionStatus.Connected) {
if (query) {
openWebSocket(`(${query}) and leftOff(-1)`, true);
} else {
openWebSocket(`leftOff(-1)`, true);
}
scrollableRef.current.jumpToBottom();
setIsSnappedToBottom(true);
}
ws.current.close();
if (wsConnection !== WsConnectionStatus.Connected) {
if (query) {
openWebSocket(`(${query}) and leftOff(-1)`, true);
} else {
openWebSocket(`leftOff(-1)`, true);
}
scrollableRef.current.jumpToBottom();
setIsSnappedToBottom(true);
}
}
const getConnectionStatusClass = (isContainer) => {
const container = isContainer ? "Container" : "";
switch (connection) {
case ConnectionStatus.Connected:
switch (wsConnection) {
case WsConnectionStatus.Connected:
return "greenIndicator" + container;
default:
return "redIndicator" + container;
}
}
const getConnectionTitle = () => {
switch (connection) {
case ConnectionStatus.Connected:
switch (wsConnection) {
case WsConnectionStatus.Connected:
return "streaming live traffic"
default:
return "streaming paused";
@@ -267,70 +237,89 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({onTLSDetected, setAnaly
const onSnapBrokenEvent = () => {
setIsSnappedToBottom(false);
if (connection === ConnectionStatus.Connected) {
if (wsConnection === WsConnectionStatus.Connected) {
ws.current.close();
}
}
return (
<div className="TrafficPage">
<div className="TrafficPageHeader">
<img className="playPauseIcon" style={{visibility: connection === ConnectionStatus.Connected ? "visible" : "hidden"}} alt="pause"
src={pauseIcon} onClick={toggleConnection}/>
<img className="playPauseIcon" style={{position: "absolute", visibility: connection === ConnectionStatus.Connected ? "hidden" : "visible"}} alt="play"
src={playIcon} onClick={toggleConnection}/>
<div className="connectionText">
{getConnectionTitle()}
<div className={"indicatorContainer " + getConnectionStatusClass(true)}>
<div className={"indicator " + getConnectionStatusClass(false)}/>
</div>
</div>
const openServiceMapModalDebounce = debounce(() => {
setOpenServiceMapModal(true)
}, 500);
return (
<div className="TrafficPage">
<div className="TrafficPageHeader">
<div className="TrafficPageStreamStatus">
<img className="playPauseIcon" style={{ visibility: wsConnection === WsConnectionStatus.Connected ? "visible" : "hidden" }} alt="pause"
src={pauseIcon} onClick={toggleConnection} />
<img className="playPauseIcon" style={{ position: "absolute", visibility: wsConnection === WsConnectionStatus.Connected ? "hidden" : "visible" }} alt="play"
src={playIcon} onClick={toggleConnection} />
<div className="connectionText">
{getConnectionTitle()}
<div className={"indicatorContainer " + getConnectionStatusClass(true)}>
<div className={"indicator " + getConnectionStatusClass(false)} />
</div>
{<div className="TrafficPage-Container">
<div className="TrafficPage-ListContainer">
<Filters
query={query}
setQuery={setQuery}
backgroundColor={queryBackgroundColor}
ws={ws.current}
openWebSocket={openWebSocket}
/>
<div className={styles.container}>
<EntriesList
entries={entries}
setEntries={setEntries}
query={query}
listEntryREF={listEntry}
onSnapBrokenEvent={onSnapBrokenEvent}
isSnappedToBottom={isSnappedToBottom}
setIsSnappedToBottom={setIsSnappedToBottom}
queriedCurrent={queriedCurrent}
setQueriedCurrent={setQueriedCurrent}
queriedTotal={queriedTotal}
setQueriedTotal={setQueriedTotal}
startTime={startTime}
noMoreDataTop={noMoreDataTop}
setNoMoreDataTop={setNoMoreDataTop}
focusedEntryId={focusedEntryId}
setFocusedEntryId={setFocusedEntryId}
updateQuery={updateQuery}
leftOffTop={leftOffTop}
setLeftOffTop={setLeftOffTop}
isWebSocketConnectionClosed={connection === ConnectionStatus.Closed}
ws={ws.current}
openWebSocket={openWebSocket}
leftOffBottom={leftOffBottom}
truncatedTimestamp={truncatedTimestamp}
setTruncatedTimestamp={setTruncatedTimestamp}
scrollableRef={scrollableRef}
/>
</div>
</div>
<div className={classes.details}>
{selectedEntryData && <EntryDetailed entryData={selectedEntryData} updateQuery={updateQuery}/>}
</div>
</div>}
{tappingStatus && <StatusBar tappingStatus={tappingStatus}/>}
</div>
</div>
)
<div style={{ display: 'flex' }}>
{window["isOasEnabled"] && <Button
type="submit"
variant="contained"
className={commonClasses.button}
style={{ marginRight: 25 }}
onClick={handleOpenModal}
>
Show OAS
</Button>}
{window["isServiceMapEnabled"] && <Button
variant="contained"
className={commonClasses.button}
onClick={openServiceMapModalDebounce}
>
Service Map
</Button>}
</div>
</div>
{window["isOasEnabled"] && <OasModal
openModal={openOasModal}
handleCloseModal={handleCloseModal}
/>}
{<div className="TrafficPage-Container">
<div className="TrafficPage-ListContainer">
<Filters
backgroundColor={queryBackgroundColor}
ws={ws.current}
openWebSocket={openWebSocket}
/>
<div className={styles.container}>
<EntriesList
listEntryREF={listEntry}
onSnapBrokenEvent={onSnapBrokenEvent}
isSnappedToBottom={isSnappedToBottom}
setIsSnappedToBottom={setIsSnappedToBottom}
queriedCurrent={queriedCurrent}
setQueriedCurrent={setQueriedCurrent}
queriedTotal={queriedTotal}
setQueriedTotal={setQueriedTotal}
startTime={startTime}
noMoreDataTop={noMoreDataTop}
setNoMoreDataTop={setNoMoreDataTop}
leftOffTop={leftOffTop}
setLeftOffTop={setLeftOffTop}
ws={ws.current}
openWebSocket={openWebSocket}
leftOffBottom={leftOffBottom}
truncatedTimestamp={truncatedTimestamp}
setTruncatedTimestamp={setTruncatedTimestamp}
scrollableRef={scrollableRef}
/>
</div>
</div>
<div className={classes.details}>
{focusedEntryId && <EntryDetailed />}
</div>
</div>}
{tappingStatus && !openOasModal && <StatusBar />}
</div>
);
};

View File

@@ -18,14 +18,12 @@ export interface ProtocolInterface {
interface ProtocolProps {
protocol: ProtocolInterface
horizontal: boolean
updateQuery: any
}
const Protocol: React.FC<ProtocolProps> = ({protocol, horizontal, updateQuery}) => {
const Protocol: React.FC<ProtocolProps> = ({protocol, horizontal}) => {
if (horizontal) {
return <Queryable
query={protocol.macro}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
>
<a target="_blank" rel="noopener noreferrer" href={protocol.referenceLink}>
@@ -45,7 +43,6 @@ const Protocol: React.FC<ProtocolProps> = ({protocol, horizontal, updateQuery})
} else {
return <Queryable
query={protocol.macro}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
flipped={false}
iconStyle={{marginTop: "52px", marginRight: "10px", zIndex: 1000}}

View File

@@ -2,10 +2,11 @@ import React, { useEffect, useState } from 'react';
import { CopyToClipboard } from 'react-copy-to-clipboard';
import AddCircleIcon from '@material-ui/icons/AddCircle';
import './style/Queryable.sass';
import {useRecoilState} from "recoil";
import queryAtom from "../../recoil/query";
interface Props {
query: string,
updateQuery: any,
style?: object,
iconStyle?: object,
className?: string,
@@ -14,9 +15,10 @@ interface Props {
flipped?: boolean,
}
const Queryable: React.FC<Props> = ({query, updateQuery, style, iconStyle, className, useTooltip= true, displayIconOnMouseOver = false, flipped = false, children}) => {
const Queryable: React.FC<Props> = ({query, style, iconStyle, className, useTooltip= true, displayIconOnMouseOver = false, flipped = false, children}) => {
const [showAddedNotification, setAdded] = useState(false);
const [showTooltip, setShowTooltip] = useState(false);
const [queryState, setQuery] = useRecoilState(queryAtom);
const onCopy = () => {
setAdded(true)
@@ -25,13 +27,15 @@ const Queryable: React.FC<Props> = ({query, updateQuery, style, iconStyle, class
useEffect(() => {
let timer;
if (showAddedNotification) {
updateQuery(query);
setQuery(queryState ? `${queryState} and ${query}` : query);
timer = setTimeout(() => {
setAdded(false);
}, 1000);
}
return () => clearTimeout(timer);
}, [showAddedNotification, query, updateQuery]);
// eslint-disable-next-line
}, [showAddedNotification, query, setQuery]);
const addButton = query ? <CopyToClipboard text={query} onCopy={onCopy}>
<span
@@ -39,7 +43,7 @@ const Queryable: React.FC<Props> = ({query, updateQuery, style, iconStyle, class
title={`Add "${query}" to the filter`}
style={iconStyle}
>
<AddCircleIcon fontSize="small" color="inherit"></AddCircleIcon>
<AddCircleIcon fontSize="small" color="inherit"/>
{showAddedNotification && <span className={'Queryable-AddNotifier'}>Added</span>}
</span>
</CopyToClipboard> : null;

View File

@@ -3,33 +3,18 @@ import React, {useState} from "react";
import warningIcon from '../assets/warning_icon.svg';
import failIcon from '../assets/failed.svg';
import successIcon from '../assets/success.svg';
export interface TappingStatusPod {
name: string;
namespace: string;
isTapped: boolean;
}
export interface TappingStatus {
pods: TappingStatusPod[];
}
export interface Props {
tappingStatus: TappingStatusPod[]
}
import {useRecoilValue} from "recoil";
import tappingStatusAtom, {tappingStatusDetails} from "../../recoil/tappingStatus";
const pluralize = (noun: string, amount: number) => {
return `${noun}${amount !== 1 ? 's' : ''}`
}
export const StatusBar: React.FC<Props> = ({tappingStatus}) => {
export const StatusBar = () => {
const tappingStatus = useRecoilValue(tappingStatusAtom);
const [expandedBar, setExpandedBar] = useState(false);
const uniqueNamespaces = Array.from(new Set(tappingStatus.map(pod => pod.namespace)));
const amountOfPods = tappingStatus.length;
const amountOfTappedPods = tappingStatus.filter(pod => pod.isTapped).length;
const amountOfUntappedPods = amountOfPods - amountOfTappedPods;
const {uniqueNamespaces, amountOfPods, amountOfTappedPods, amountOfUntappedPods} = useRecoilValue(tappingStatusDetails);
return <div className={'statusBar' + (expandedBar ? ' expandedStatusBar' : "")} onMouseOver={() => setExpandedBar(true)} onMouseLeave={() => setExpandedBar(false)}>
<div className="podsCount">

View File

@@ -10,16 +10,14 @@ export enum StatusCodeClassification {
interface EntryProps {
statusCode: number
updateQuery: any
}
const StatusCode: React.FC<EntryProps> = ({statusCode, updateQuery}) => {
const StatusCode: React.FC<EntryProps> = ({statusCode}) => {
const classification = getClassification(statusCode)
return <Queryable
query={`response.status == ${statusCode}`}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
flipped={true}
iconStyle={{marginTop: "40px", paddingLeft: "10px"}}
@@ -37,7 +35,7 @@ export function getClassification(statusCode: number): string {
let classification = StatusCodeClassification.NEUTRAL;
// 1 - 16 HTTP/2 (gRPC) status codes
// 2xx - 5xx HTTP/1.1 status codes
// 2xx - 5xx HTTP/1.x status codes
if ((statusCode >= 200 && statusCode <= 399) || statusCode === 0) {
classification = StatusCodeClassification.SUCCESS;
} else if (statusCode >= 400 || (statusCode >= 1 && statusCode <= 16)) {

View File

@@ -6,16 +6,14 @@ import Queryable from "./Queryable";
interface SummaryProps {
method: string
summary: string
updateQuery: any
}
export const Summary: React.FC<SummaryProps> = ({method, summary, updateQuery}) => {
export const Summary: React.FC<SummaryProps> = ({method, summary}) => {
return <div className={styles.container}>
{method && <Queryable
query={`method == "${method}"`}
className={`${miscStyles.protocol} ${miscStyles.method}`}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
style={{whiteSpace: "nowrap"}}
>
@@ -25,7 +23,6 @@ export const Summary: React.FC<SummaryProps> = ({method, summary, updateQuery})
</Queryable>}
{summary && <Queryable
query={`summary == "${summary}"`}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
>
<div

View File

@@ -2,4 +2,4 @@
fill: #627ef7
.list
margin-top: 8px
margin-top: 8px

View File

@@ -0,0 +1,4 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.5 11C20.5 16.2467 16.2467 20.5 11 20.5C5.75329 20.5 1.5 16.2467 1.5 11C1.5 5.75329 5.75329 1.5 11 1.5C16.2467 1.5 20.5 5.75329 20.5 11Z" stroke="#2B3560"/>
<path d="M14.4762 9.05338L13.1448 7.7219L11.1528 9.71382L9.16091 7.7219L7.82943 9.05338L9.82135 11.0453L7.83226 13.0344L9.16374 14.3659L11.1528 12.3768L13.1419 14.3659L14.4734 13.0344L12.4843 11.0453L14.4762 9.05338Z" fill="#627EF7"/>
</svg>

After

Width:  |  Height:  |  Size: 507 B

View File

@@ -0,0 +1,7 @@
@import "../../variables.module"
.spinnerContainer
display: flex
justify-content: center
margin-bottom: 10px

View File

@@ -13,7 +13,12 @@
display: flex
align-items: center
background-color: $header-background-color
justify-content: space-between
.TrafficPageStreamStatus
display: flex
align-items: center
.TrafficPage-Header
display: flex
height: 2.5%

View File

@@ -0,0 +1,7 @@
const dictionary = {
ctrlEnter : [{metaKey : true, code:"Enter"}, {ctrlKey:true, code:"Enter"}], // support Ctrl/command
enter : [{code:"Enter"}]
};
export default dictionary;

View File

@@ -28,6 +28,21 @@ export default class Api {
this.source = null;
}
serviceMapStatus = async () => {
const response = await this.client.get("/servicemap/status");
return response.data;
}
serviceMapData = async () => {
const response = await this.client.get(`/servicemap/get`);
return response.data;
}
serviceMapReset = async () => {
const response = await this.client.get(`/servicemap/reset`);
return response.data;
}
tapStatus = async () => {
const response = await this.client.get("/status/tap");
return response.data;
@@ -61,6 +76,16 @@ export default class Api {
return response.data;
}
getOasServices = async () => {
const response = await this.client.get("/oas");
return response.data;
}
getOasByService = async (selectedService) => {
const response = await this.client.get(`/oas/${selectedService}`);
return response.data;
}
validateQuery = async (query) => {
if (this.source) {
this.source.cancel();

View File

@@ -0,0 +1,35 @@
import { useCallback, useEffect, useLayoutEffect, useRef } from 'react';
const useKeyPress = (eventConfigs, callback, node = null) => {
// implement the callback ref pattern
const callbackRef = useRef(callback);
useLayoutEffect(() => {
callbackRef.current = callback;
});
// handle what happens on key press
const handleKeyPress = useCallback(
(event) => {
// check if one of the key is part of the ones we want
if (eventConfigs.some((eventConfig) => Object.keys(eventConfig).every(nameKey => eventConfig[nameKey] === event[nameKey]))) {
callbackRef.current(event);
}
},
[eventConfigs]
);
useEffect(() => {
// target is either the provided node or the document
const targetNode = node ?? document;
// attach the event listener
targetNode &&
targetNode.addEventListener("keydown", handleKeyPress);
// remove the event listener
return () =>
targetNode &&
targetNode.removeEventListener("keydown", handleKeyPress);
}, [handleKeyPress, node]);
};
export default useKeyPress;

View File

@@ -5,23 +5,26 @@ import App from './App';
import EntApp from "./EntApp";
import {ToastContainer} from "react-toastify";
import 'react-toastify/dist/ReactToastify.css';
import {RecoilRoot} from "recoil";
ReactDOM.render(
<React.StrictMode>
<>
{window["isEnt"] ? <EntApp/> : <App/>}
<ToastContainer
position="bottom-right"
autoClose={5000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
/>
</>
<RecoilRoot>
<>
{window["isEnt"] ? <EntApp/> : <App/>}
<ToastContainer
position="bottom-right"
autoClose={5000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
/>
</>
</RecoilRoot>
</React.StrictMode>,
document.getElementById('root')
);

View File

@@ -0,0 +1,8 @@
import { atom } from "recoil"
const entPageAtom = atom({
key: "entPageAtom",
default: 0
})
export default entPageAtom

View File

@@ -0,0 +1,11 @@
import atom from "./atom";
enum Page {
Traffic,
Setup,
Login
}
export { Page };
export default atom;

View File

@@ -0,0 +1,8 @@
import { atom } from "recoil";
const entriesAtom = atom({
key: "entriesAtom",
default: []
});
export default entriesAtom;

View File

@@ -0,0 +1,3 @@
import atom from "./atom";
export default atom

View File

@@ -0,0 +1,8 @@
import { atom } from "recoil";
const focusedEntryIdAtom = atom({
key: "focusedEntryIdAtom",
default: null
});
export default focusedEntryIdAtom;

View File

@@ -0,0 +1,3 @@
import atom from "./atom";
export default atom

View File

@@ -0,0 +1,8 @@
import { atom } from "recoil";
const queryAtom = atom({
key: "queryAtom",
default: ""
});
export default queryAtom;

View File

@@ -0,0 +1,3 @@
import atom from "./atom";
export default atom;

View File

@@ -0,0 +1,9 @@
import { atom } from "recoil";
import {TappingStatusPod} from "./index";
const tappingStatusAtom = atom({
key: "tappingStatusAtom",
default: null as TappingStatusPod[]
});
export default tappingStatusAtom;

View File

@@ -0,0 +1,22 @@
import {selector} from "recoil";
import tappingStatusAtom from "./atom";
const tappingStatusDetails = selector({
key: 'tappingStatusDetails',
get: ({get}) => {
const tappingStatus = get(tappingStatusAtom);
const uniqueNamespaces = Array.from(new Set(tappingStatus.map(pod => pod.namespace)));
const amountOfPods = tappingStatus.length;
const amountOfTappedPods = tappingStatus.filter(pod => pod.isTapped).length;
const amountOfUntappedPods = amountOfPods - amountOfTappedPods;
return {
uniqueNamespaces,
amountOfPods,
amountOfTappedPods,
amountOfUntappedPods,
};
},
});
export default tappingStatusDetails;

View File

@@ -0,0 +1,17 @@
import atom from "./atom";
import tappingStatusDetails from './details';
interface TappingStatusPod {
name: string;
namespace: string;
isTapped: boolean;
}
interface TappingStatus {
pods: TappingStatusPod[];
}
export type {TappingStatus, TappingStatusPod};
export {tappingStatusDetails};
export default atom;

View File

@@ -0,0 +1,8 @@
import { atom } from "recoil";
const wsConnectionAtom = atom({
key: "wsConnectionAtom",
default: 0
});
export default wsConnectionAtom;

View File

@@ -0,0 +1,10 @@
import atom from "./atom";
enum WsConnectionStatus {
Closed,
Connected,
}
export {WsConnectionStatus};
export default atom