mirror of
https://github.com/kubeshark/kubeshark.git
synced 2026-02-20 04:50:20 +00:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90c210452d | ||
|
|
0a915b3fe7 | ||
|
|
a830bbe023 | ||
|
|
f1ba397543 | ||
|
|
4e17ac5654 | ||
|
|
d274db2d87 | ||
|
|
0a2aacfb02 | ||
|
|
3c64c1c7ca | ||
|
|
005f000ef6 | ||
|
|
1ef3778051 | ||
|
|
9f1e311689 | ||
|
|
9aaf18842b | ||
|
|
880842c39f | ||
|
|
296e1bb667 | ||
|
|
2910611111 | ||
|
|
c47959dbd8 | ||
|
|
af557f7052 | ||
|
|
b745f65971 | ||
|
|
873f252544 | ||
|
|
9696ad9bad | ||
|
|
a1bda0a6c3 | ||
|
|
a62842ac9f | ||
|
|
e667597e6e |
@@ -42,8 +42,8 @@ RUN go build -ldflags="-s -w \
|
||||
-X 'mizuserver/pkg/version.SemVer=${SEM_VER}'" -o mizuagent .
|
||||
|
||||
# Download Basenine executable, verify the sha1sum and move it to a directory in $PATH
|
||||
ADD https://github.com/up9inc/basenine/releases/download/v0.2.11/basenine_linux_amd64 ./basenine_linux_amd64
|
||||
ADD https://github.com/up9inc/basenine/releases/download/v0.2.11/basenine_linux_amd64.sha256 ./basenine_linux_amd64.sha256
|
||||
ADD https://github.com/up9inc/basenine/releases/download/v0.2.17/basenine_linux_amd64 ./basenine_linux_amd64
|
||||
ADD https://github.com/up9inc/basenine/releases/download/v0.2.17/basenine_linux_amd64.sha256 ./basenine_linux_amd64.sha256
|
||||
RUN shasum -a 256 -c basenine_linux_amd64.sha256
|
||||
RUN chmod +x ./basenine_linux_amd64
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ require (
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||
github.com/orcaman/concurrent-map v0.0.0-20210106121528-16402b402231
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/up9inc/basenine/client/go v0.0.0-20211121072216-04366911881c
|
||||
github.com/up9inc/basenine/client/go v0.0.0-20211207165834-2ced7577f9e6
|
||||
github.com/up9inc/mizu/shared v0.0.0
|
||||
github.com/up9inc/mizu/tap v0.0.0
|
||||
github.com/up9inc/mizu/tap/api v0.0.0
|
||||
|
||||
@@ -450,8 +450,8 @@ github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/up9inc/basenine/client/go v0.0.0-20211121072216-04366911881c h1:GJsCVhDKjV/k3mNG255VN7hAQ7fxyNgX5T+VJyzoOQ0=
|
||||
github.com/up9inc/basenine/client/go v0.0.0-20211121072216-04366911881c/go.mod h1:SvJGPoa/6erhUQV7kvHBwM/0x5LyO6XaG2lUaCaKiUI=
|
||||
github.com/up9inc/basenine/client/go v0.0.0-20211207165834-2ced7577f9e6 h1:8JOkoaZHhUPi4r7vSL/xo83foSz8BHPSabTDpxmtHFU=
|
||||
github.com/up9inc/basenine/client/go v0.0.0-20211207165834-2ced7577f9e6/go.mod h1:SvJGPoa/6erhUQV7kvHBwM/0x5LyO6XaG2lUaCaKiUI=
|
||||
github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
|
||||
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f h1:p4VB7kIXpOQvVn1ZaTIVp+3vuYAXFe3OJEvjbUYJLaA=
|
||||
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"plugin"
|
||||
"regexp"
|
||||
"sort"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -273,8 +272,6 @@ func hostApi(socketHarOutputChannel chan<- *tapApi.OutputChannelItem) {
|
||||
if _, err := startMizuTapperSyncer(ctx, kubernetesProvider); err != nil {
|
||||
logger.Log.Fatalf("error initializing tapper syncer: %+v", err)
|
||||
}
|
||||
|
||||
go watchMizuEvents(ctx, kubernetesProvider, cancel)
|
||||
}
|
||||
|
||||
utils.StartServer(app)
|
||||
@@ -447,7 +444,7 @@ func startMizuTapperSyncer(ctx context.Context, provider *kubernetes.Provider) (
|
||||
MizuApiFilteringOptions: config.Config.MizuApiFilteringOptions,
|
||||
MizuServiceAccountExists: true, //assume service account exists since daemon mode will not function without it anyway
|
||||
Istio: config.Config.Istio,
|
||||
})
|
||||
}, time.Now())
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -477,6 +474,16 @@ func startMizuTapperSyncer(ctx context.Context, provider *kubernetes.Provider) (
|
||||
api.BroadcastToBrowserClients(serializedTapStatus)
|
||||
providers.TapStatus.Pods = tapStatus.Pods
|
||||
providers.ExpectedTapperAmount = tapPodChangeEvent.ExpectedTapperAmount
|
||||
case tapperStatus, ok := <-tapperSyncer.TapperStatusChangedOut:
|
||||
if !ok {
|
||||
logger.Log.Debug("mizuTapperSyncer tapper status changed channel closed, ending listener loop")
|
||||
return
|
||||
}
|
||||
if providers.TappersStatus == nil {
|
||||
providers.TappersStatus = make(map[string]shared.TapperStatus)
|
||||
}
|
||||
providers.TappersStatus[tapperStatus.NodeName] = tapperStatus
|
||||
|
||||
case <-ctx.Done():
|
||||
logger.Log.Debug("mizuTapperSyncer event listener loop exiting due to context done")
|
||||
return
|
||||
@@ -486,48 +493,3 @@ func startMizuTapperSyncer(ctx context.Context, provider *kubernetes.Provider) (
|
||||
|
||||
return tapperSyncer, nil
|
||||
}
|
||||
|
||||
func watchMizuEvents(ctx context.Context, kubernetesProvider *kubernetes.Provider, cancel context.CancelFunc) {
|
||||
// Round down because k8s CreationTimestamp is given in 1 sec resolution.
|
||||
startTime := time.Now().Truncate(time.Second)
|
||||
|
||||
mizuResourceRegex := regexp.MustCompile(fmt.Sprintf("^%s.*", kubernetes.MizuResourcesPrefix))
|
||||
eventWatchHelper := kubernetes.NewEventWatchHelper(kubernetesProvider, mizuResourceRegex)
|
||||
eventChan, errorChan := kubernetes.FilteredWatch(ctx, eventWatchHelper, []string{config.Config.MizuResourcesNamespace}, eventWatchHelper)
|
||||
|
||||
for {
|
||||
select {
|
||||
case wEvent, ok := <-eventChan:
|
||||
if !ok {
|
||||
eventChan = nil
|
||||
continue
|
||||
}
|
||||
|
||||
event, err := wEvent.ToEvent()
|
||||
if err != nil {
|
||||
logger.Log.Errorf("error parsing Mizu resource event: %+v", err)
|
||||
cancel()
|
||||
}
|
||||
|
||||
if startTime.After(event.CreationTimestamp.Time) {
|
||||
continue
|
||||
}
|
||||
|
||||
if event.Type == v1.EventTypeWarning {
|
||||
logger.Log.Warningf("resource %s in state %s - %s", event.Regarding.Name, event.Reason, event.Note)
|
||||
}
|
||||
case err, ok := <-errorChan:
|
||||
if !ok {
|
||||
errorChan = nil
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Log.Errorf("error in watch mizu resource events loop: %+v", err)
|
||||
cancel()
|
||||
|
||||
case <-ctx.Done():
|
||||
logger.Log.Debugf("watching Mizu resource events loop, ctx done")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ func startReadingChannel(outputItems <-chan *tapApi.OutputChannelItem, extension
|
||||
|
||||
harEntry, err := utils.NewEntry(mizuEntry.Request, mizuEntry.Response, mizuEntry.StartTime, mizuEntry.ElapsedTime)
|
||||
if err == nil {
|
||||
rules, _, _ := models.RunValidationRulesState(*harEntry, mizuEntry.Service)
|
||||
rules, _, _ := models.RunValidationRulesState(*harEntry, mizuEntry.Destination.Name)
|
||||
baseEntry.Rules = rules
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,8 +127,15 @@ func websocketHandler(w http.ResponseWriter, r *http.Request, eventHandlers Even
|
||||
var dataMap map[string]interface{}
|
||||
err = json.Unmarshal(bytes, &dataMap)
|
||||
|
||||
base := dataMap["base"].(map[string]interface{})
|
||||
base["id"] = uint(dataMap["id"].(float64))
|
||||
var base map[string]interface{}
|
||||
switch dataMap["base"].(type) {
|
||||
case map[string]interface{}:
|
||||
base = dataMap["base"].(map[string]interface{})
|
||||
base["id"] = uint(dataMap["id"].(float64))
|
||||
default:
|
||||
logger.Log.Debugf("Base field has an unrecognized type: %+v", dataMap)
|
||||
continue
|
||||
}
|
||||
|
||||
baseEntryBytes, _ := models.CreateBaseEntryWebSocketMessage(base)
|
||||
SendToSocket(socketId, baseEntryBytes)
|
||||
|
||||
@@ -4,8 +4,10 @@ import (
|
||||
"encoding/json"
|
||||
"mizuserver/pkg/models"
|
||||
"mizuserver/pkg/utils"
|
||||
"mizuserver/pkg/validation"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
@@ -36,6 +38,62 @@ func Error(c *gin.Context, err error) bool {
|
||||
return false // no error, can continue
|
||||
}
|
||||
|
||||
func GetEntries(c *gin.Context) {
|
||||
entriesRequest := &models.EntriesRequest{}
|
||||
|
||||
if err := c.BindQuery(entriesRequest); err != nil {
|
||||
c.JSON(http.StatusBadRequest, err)
|
||||
}
|
||||
validationError := validation.Validate(entriesRequest)
|
||||
if validationError != nil {
|
||||
c.JSON(http.StatusBadRequest, validationError)
|
||||
}
|
||||
|
||||
if entriesRequest.TimeoutMs == 0 {
|
||||
entriesRequest.TimeoutMs = 3000
|
||||
}
|
||||
|
||||
data, meta, err := basenine.Fetch(shared.BasenineHost, shared.BaseninePort,
|
||||
entriesRequest.LeftOff, entriesRequest.Direction, entriesRequest.Query,
|
||||
entriesRequest.Limit, time.Duration(entriesRequest.TimeoutMs)*time.Millisecond)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, validationError)
|
||||
}
|
||||
|
||||
response := &models.EntriesResponse{}
|
||||
var dataSlice []interface{}
|
||||
|
||||
for _, row := range data {
|
||||
var dataMap map[string]interface{}
|
||||
err = json.Unmarshal(row, &dataMap)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": true,
|
||||
"type": "error",
|
||||
"autoClose": "5000",
|
||||
"msg": string(row),
|
||||
})
|
||||
return // exit
|
||||
}
|
||||
|
||||
base := dataMap["base"].(map[string]interface{})
|
||||
base["id"] = uint(dataMap["id"].(float64))
|
||||
|
||||
dataSlice = append(dataSlice, base)
|
||||
}
|
||||
|
||||
var metadata *basenine.Metadata
|
||||
err = json.Unmarshal(meta, &metadata)
|
||||
if err != nil {
|
||||
logger.Log.Debugf("Error recieving metadata: %v", err.Error())
|
||||
}
|
||||
|
||||
response.Data = dataSlice
|
||||
response.Meta = metadata
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
func GetEntry(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var entry tapApi.MizuEntry
|
||||
@@ -61,7 +119,7 @@ func GetEntry(c *gin.Context) {
|
||||
var isRulesEnabled bool
|
||||
if entry.Protocol.Name == "http" {
|
||||
harEntry, _ := utils.NewEntry(entry.Request, entry.Response, entry.StartTime, entry.ElapsedTime)
|
||||
_, rulesMatched, _isRulesEnabled := models.RunValidationRulesState(*harEntry, entry.Service)
|
||||
_, rulesMatched, _isRulesEnabled := models.RunValidationRulesState(*harEntry, entry.Destination.Name)
|
||||
isRulesEnabled = _isRulesEnabled
|
||||
inrec, _ := json.Marshal(rulesMatched)
|
||||
json.Unmarshal(inrec, &rules)
|
||||
|
||||
@@ -24,14 +24,19 @@ func HealthCheck(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
tappers := make([]shared.TapperStatus, len(providers.TappersStatus))
|
||||
for _, value := range providers.TappersStatus {
|
||||
tappers = append(tappers, value)
|
||||
}
|
||||
|
||||
response := shared.HealthResponse{
|
||||
TapStatus: providers.TapStatus,
|
||||
TappersCount: providers.TappersCount,
|
||||
TapStatus: providers.TapStatus,
|
||||
TappersCount: providers.TappersCount,
|
||||
TappersStatus: tappers,
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
|
||||
func PostTappedPods(c *gin.Context) {
|
||||
tapStatus := &shared.TapStatus{}
|
||||
if err := c.Bind(tapStatus); err != nil {
|
||||
@@ -52,6 +57,23 @@ func PostTappedPods(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func PostTapperStatus(c *gin.Context) {
|
||||
tapperStatus := &shared.TapperStatus{}
|
||||
if err := c.Bind(tapperStatus); err != nil {
|
||||
c.JSON(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
if err := validation.Validate(tapperStatus); err != nil {
|
||||
c.JSON(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
logger.Log.Infof("[Status] POST request, tapper status: %v", tapperStatus)
|
||||
if providers.TappersStatus == nil {
|
||||
providers.TappersStatus = make(map[string]shared.TapperStatus)
|
||||
}
|
||||
providers.TappersStatus[tapperStatus.NodeName] = *tapperStatus
|
||||
}
|
||||
|
||||
func GetTappersCount(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, providers.TappersCount)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,19 @@ func GetEntry(r *tapApi.MizuEntry, v tapApi.DataUnmarshaler) error {
|
||||
return v.UnmarshalData(r)
|
||||
}
|
||||
|
||||
type EntriesRequest struct {
|
||||
LeftOff int `form:"leftOff" validate:"required,min=-1"`
|
||||
Direction int `form:"direction" validate:"required,oneof='1' '-1'"`
|
||||
Query string `form:"query"`
|
||||
Limit int `form:"limit" validate:"required,min=1"`
|
||||
TimeoutMs int `form:"timeoutMs" validate:"min=1"`
|
||||
}
|
||||
|
||||
type EntriesResponse struct {
|
||||
Data []interface{} `json:"data"`
|
||||
Meta *basenine.Metadata `json:"meta"`
|
||||
}
|
||||
|
||||
type WebSocketEntryMessage struct {
|
||||
*shared.WebSocketMessageMetadata
|
||||
Data map[string]interface{} `json:"data,omitempty"`
|
||||
|
||||
@@ -15,12 +15,13 @@ import (
|
||||
const tlsLinkRetainmentTime = time.Minute * 15
|
||||
|
||||
var (
|
||||
TappersCount int
|
||||
TapStatus shared.TapStatus
|
||||
authStatus *models.AuthStatus
|
||||
RecentTLSLinks = cache.New(tlsLinkRetainmentTime, tlsLinkRetainmentTime)
|
||||
TappersCount int
|
||||
TapStatus shared.TapStatus
|
||||
TappersStatus map[string]shared.TapperStatus
|
||||
authStatus *models.AuthStatus
|
||||
RecentTLSLinks = cache.New(tlsLinkRetainmentTime, tlsLinkRetainmentTime)
|
||||
ExpectedTapperAmount = -1 //only relevant in daemon mode as cli manages tappers otherwise
|
||||
tappersCountLock = sync.Mutex{}
|
||||
tappersCountLock = sync.Mutex{}
|
||||
)
|
||||
|
||||
func GetAuthStatus() (*models.AuthStatus, error) {
|
||||
|
||||
@@ -10,5 +10,6 @@ import (
|
||||
func EntriesRoutes(ginApp *gin.Engine) {
|
||||
routeGroup := ginApp.Group("/entries")
|
||||
|
||||
routeGroup.GET("/", controllers.GetEntries) // get entries (base/thin entries) and metadata
|
||||
routeGroup.GET("/:id", controllers.GetEntry) // get single (full) entry
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ func StatusRoutes(ginApp *gin.Engine) {
|
||||
routeGroup.GET("/health", controllers.HealthCheck)
|
||||
|
||||
routeGroup.POST("/tappedPods", controllers.PostTappedPods)
|
||||
routeGroup.POST("/tapperStatus", controllers.PostTapperStatus)
|
||||
routeGroup.GET("/tappersCount", controllers.GetTappersCount)
|
||||
routeGroup.GET("/tap", controllers.GetTappingStatus)
|
||||
|
||||
|
||||
@@ -251,12 +251,12 @@ func syncEntriesImpl(token string, model string, envPrefix string, uploadInterva
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if entry.ResolvedSource != "" {
|
||||
harEntry.Request.Headers = append(harEntry.Request.Headers, har.Header{Name: "x-mizu-source", Value: entry.ResolvedSource})
|
||||
if entry.Source.Name != "" {
|
||||
harEntry.Request.Headers = append(harEntry.Request.Headers, har.Header{Name: "x-mizu-source", Value: entry.Source.Name})
|
||||
}
|
||||
if entry.ResolvedDestination != "" {
|
||||
harEntry.Request.Headers = append(harEntry.Request.Headers, har.Header{Name: "x-mizu-destination", Value: entry.ResolvedDestination})
|
||||
harEntry.Request.URL = utils.SetHostname(harEntry.Request.URL, entry.ResolvedDestination)
|
||||
if entry.Destination.Name != "" {
|
||||
harEntry.Request.Headers = append(harEntry.Request.Headers, har.Header{Name: "x-mizu-destination", Value: entry.Destination.Name})
|
||||
harEntry.Request.URL = utils.SetHostname(harEntry.Request.URL, entry.Destination.Name)
|
||||
}
|
||||
|
||||
// go's default marshal behavior is to encode []byte fields to base64, python's default unmarshal behavior is to not decode []byte fields from base64
|
||||
|
||||
@@ -18,8 +18,9 @@ build: ## Build mizu CLI binary (select platform via GOOS / GOARCH env variables
|
||||
go build -ldflags="-X 'github.com/up9inc/mizu/cli/mizu.GitCommitHash=$(COMMIT_HASH)' \
|
||||
-X 'github.com/up9inc/mizu/cli/mizu.Branch=$(GIT_BRANCH)' \
|
||||
-X 'github.com/up9inc/mizu/cli/mizu.BuildTimestamp=$(BUILD_TIMESTAMP)' \
|
||||
-X 'github.com/up9inc/mizu/cli/mizu.Platform=$(SUFFIX)' \
|
||||
-X 'github.com/up9inc/mizu/cli/mizu.SemVer=$(SEM_VER)'" \
|
||||
-o bin/mizu_$(SUFFIX) mizu.go
|
||||
-o bin/mizu_$(SUFFIX) mizu.go
|
||||
(cd bin && shasum -a 256 mizu_${SUFFIX} > mizu_${SUFFIX}.sha256)
|
||||
|
||||
build-all: ## Build for all supported platforms.
|
||||
|
||||
@@ -42,7 +42,7 @@ func (provider *Provider) TestConnection() error {
|
||||
retriesLeft := provider.retries
|
||||
for retriesLeft > 0 {
|
||||
if _, err := provider.GetHealthStatus(); err != nil {
|
||||
logger.Log.Debugf("[ERROR] api server not ready yet %v", err)
|
||||
logger.Log.Debugf("api server not ready yet %v", err)
|
||||
} else {
|
||||
logger.Log.Debugf("connection test to api server passed successfully")
|
||||
break
|
||||
@@ -81,6 +81,23 @@ func (provider *Provider) GetHealthStatus() (*shared.HealthResponse, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *Provider) ReportTapperStatus(tapperStatus shared.TapperStatus) error {
|
||||
tapperStatusUrl := fmt.Sprintf("%s/status/tapperStatus", provider.url)
|
||||
|
||||
if jsonValue, err := json.Marshal(tapperStatus); err != nil {
|
||||
return fmt.Errorf("failed Marshal the tapper status %w", err)
|
||||
} else {
|
||||
if response, err := provider.client.Post(tapperStatusUrl, "application/json", bytes.NewBuffer(jsonValue)); err != nil {
|
||||
return fmt.Errorf("failed sending to API server the tapped pods %w", err)
|
||||
} else if response.StatusCode != 200 {
|
||||
return fmt.Errorf("failed sending to API server the tapper status, response status code %v", response.StatusCode)
|
||||
} else {
|
||||
logger.Log.Debugf("Reported to server API about tapper status: %v", tapperStatus)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *Provider) ReportTappedPods(pods []core.Pod) error {
|
||||
tappedPodsUrl := fmt.Sprintf("%s/status/tappedPods", provider.url)
|
||||
|
||||
|
||||
@@ -33,6 +33,9 @@ import (
|
||||
const cleanupTimeout = time.Minute
|
||||
|
||||
type tapState struct {
|
||||
startTime time.Time
|
||||
targetNamespaces []string
|
||||
|
||||
apiServerService *core.Service
|
||||
tapperSyncer *kubernetes.MizuTapperSyncer
|
||||
mizuServiceAccountExists bool
|
||||
@@ -42,7 +45,7 @@ var state tapState
|
||||
var apiProvider *apiserver.Provider
|
||||
|
||||
func RunMizuTap() {
|
||||
startTime := time.Now()
|
||||
state.startTime = time.Now()
|
||||
|
||||
mizuApiFilteringOptions, err := getMizuApiFilteringOptions()
|
||||
apiProvider = apiserver.NewProvider(GetApiServerUrl(), apiserver.DefaultRetries, apiserver.DefaultTimeout)
|
||||
@@ -92,16 +95,16 @@ func RunMizuTap() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel() // cancel will be called when this function exits
|
||||
|
||||
targetNamespaces := getNamespaces(kubernetesProvider)
|
||||
state.targetNamespaces = getNamespaces(kubernetesProvider)
|
||||
|
||||
serializedMizuConfig, err := config.GetSerializedMizuAgentConfig(targetNamespaces, mizuApiFilteringOptions)
|
||||
serializedMizuConfig, err := config.GetSerializedMizuAgentConfig(state.targetNamespaces, mizuApiFilteringOptions)
|
||||
if err != nil {
|
||||
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error composing mizu config: %v", errormessage.FormatError(err)))
|
||||
return
|
||||
}
|
||||
|
||||
if config.Config.IsNsRestrictedMode() {
|
||||
if len(targetNamespaces) != 1 || !shared.Contains(targetNamespaces, config.Config.MizuResourcesNamespace) {
|
||||
if len(state.targetNamespaces) != 1 || !shared.Contains(state.targetNamespaces, config.Config.MizuResourcesNamespace) {
|
||||
logger.Log.Errorf("Not supported mode. Mizu can't resolve IPs in other namespaces when running in namespace restricted mode.\n"+
|
||||
"You can use the same namespace for --%s and --%s", configStructs.NamespacesTapName, config.MizuResourcesNamespaceConfigName)
|
||||
return
|
||||
@@ -109,18 +112,19 @@ func RunMizuTap() {
|
||||
}
|
||||
|
||||
var namespacesStr string
|
||||
if !shared.Contains(targetNamespaces, kubernetes.K8sAllNamespaces) {
|
||||
namespacesStr = fmt.Sprintf("namespaces \"%s\"", strings.Join(targetNamespaces, "\", \""))
|
||||
if !shared.Contains(state.targetNamespaces, kubernetes.K8sAllNamespaces) {
|
||||
namespacesStr = fmt.Sprintf("namespaces \"%s\"", strings.Join(state.targetNamespaces, "\", \""))
|
||||
} else {
|
||||
namespacesStr = "all namespaces"
|
||||
}
|
||||
|
||||
logger.Log.Infof("Tapping pods in %s", namespacesStr)
|
||||
|
||||
if err := printTappedPodsPreview(ctx, kubernetesProvider, state.targetNamespaces); err != nil {
|
||||
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error listing pods: %v", errormessage.FormatError(err)))
|
||||
}
|
||||
|
||||
if config.Config.Tap.DryRun {
|
||||
if err := printTappedPodsPreview(ctx, kubernetesProvider, targetNamespaces); err != nil {
|
||||
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error listing pods: %v", errormessage.FormatError(err)))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -136,7 +140,7 @@ func RunMizuTap() {
|
||||
return
|
||||
}
|
||||
if config.Config.Tap.DaemonMode {
|
||||
if err := handleDaemonModePostCreation(ctx, cancel, kubernetesProvider, targetNamespaces); err != nil {
|
||||
if err := handleDaemonModePostCreation(ctx, cancel, kubernetesProvider, state.targetNamespaces); err != nil {
|
||||
defer finishMizuExecution(kubernetesProvider, apiProvider)
|
||||
cancel()
|
||||
} else {
|
||||
@@ -145,14 +149,7 @@ func RunMizuTap() {
|
||||
} else {
|
||||
defer finishMizuExecution(kubernetesProvider, apiProvider)
|
||||
|
||||
if err = startTapperSyncer(ctx, cancel, kubernetesProvider, targetNamespaces, *mizuApiFilteringOptions); err != nil {
|
||||
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error starting mizu tapper syncer: %v", err))
|
||||
cancel()
|
||||
}
|
||||
|
||||
go goUtils.HandleExcWrapper(watchApiServerPod, ctx, kubernetesProvider, cancel)
|
||||
go goUtils.HandleExcWrapper(watchTapperPod, ctx, kubernetesProvider, cancel)
|
||||
go goUtils.HandleExcWrapper(watchMizuEvents, ctx, kubernetesProvider, cancel, startTime)
|
||||
|
||||
// block until exit signal or error
|
||||
waitForFinish(ctx, cancel)
|
||||
@@ -185,7 +182,6 @@ func printTappedPodsPreview(ctx context.Context, kubernetesProvider *kubernetes.
|
||||
if len(matchingPods) == 0 {
|
||||
printNoPodsFoundSuggestion(namespaces)
|
||||
}
|
||||
logger.Log.Info("Pods that match the provided criteria at this instant:")
|
||||
for _, tappedPod := range matchingPods {
|
||||
logger.Log.Infof(uiUtils.Green, fmt.Sprintf("+%s", tappedPod.Name))
|
||||
}
|
||||
@@ -205,7 +201,7 @@ func waitForDaemonModeToBeReady(cancel context.CancelFunc, kubernetesProvider *k
|
||||
return nil
|
||||
}
|
||||
|
||||
func startTapperSyncer(ctx context.Context, cancel context.CancelFunc, provider *kubernetes.Provider, targetNamespaces []string, mizuApiFilteringOptions api.TrafficFilteringOptions) error {
|
||||
func startTapperSyncer(ctx context.Context, cancel context.CancelFunc, provider *kubernetes.Provider, targetNamespaces []string, mizuApiFilteringOptions api.TrafficFilteringOptions, startTime time.Time) error {
|
||||
tapperSyncer, err := kubernetes.CreateAndStartMizuTapperSyncer(ctx, provider, kubernetes.TapperSyncerConfig{
|
||||
TargetNamespaces: targetNamespaces,
|
||||
PodFilterRegex: *config.Config.Tap.PodRegex(),
|
||||
@@ -218,20 +214,12 @@ func startTapperSyncer(ctx context.Context, cancel context.CancelFunc, provider
|
||||
MizuApiFilteringOptions: mizuApiFilteringOptions,
|
||||
MizuServiceAccountExists: state.mizuServiceAccountExists,
|
||||
Istio: config.Config.Tap.Istio,
|
||||
})
|
||||
}, startTime)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, tappedPod := range tapperSyncer.CurrentlyTappedPods {
|
||||
logger.Log.Infof(uiUtils.Green, fmt.Sprintf("+%s", tappedPod.Name))
|
||||
}
|
||||
|
||||
if len(tapperSyncer.CurrentlyTappedPods) == 0 {
|
||||
printNoPodsFoundSuggestion(targetNamespaces)
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
@@ -250,6 +238,14 @@ func startTapperSyncer(ctx context.Context, cancel context.CancelFunc, provider
|
||||
if err := apiProvider.ReportTappedPods(tapperSyncer.CurrentlyTappedPods); err != nil {
|
||||
logger.Log.Debugf("[Error] failed update tapped pods %v", err)
|
||||
}
|
||||
case tapperStatus, ok := <-tapperSyncer.TapperStatusChangedOut:
|
||||
if !ok {
|
||||
logger.Log.Debug("mizuTapperSyncer tapper status changed channel closed, ending listener loop")
|
||||
return
|
||||
}
|
||||
if err := apiProvider.ReportTapperStatus(tapperStatus); err != nil {
|
||||
logger.Log.Debugf("[Error] failed update tapper status %v", err)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
logger.Log.Debug("mizuTapperSyncer event listener loop exiting due to context done")
|
||||
return
|
||||
@@ -557,171 +553,9 @@ func waitUntilNamespaceDeleted(ctx context.Context, cancel context.CancelFunc, k
|
||||
}
|
||||
|
||||
func watchApiServerPod(ctx context.Context, kubernetesProvider *kubernetes.Provider, cancel context.CancelFunc) {
|
||||
podExactRegex := regexp.MustCompile(fmt.Sprintf("^%s$", kubernetes.ApiServerPodName))
|
||||
podWatchHelper := kubernetes.NewPodWatchHelper(kubernetesProvider, podExactRegex)
|
||||
eventChan, errorChan := kubernetes.FilteredWatch(ctx, podWatchHelper, []string{config.Config.MizuResourcesNamespace}, podWatchHelper)
|
||||
isPodReady := false
|
||||
timeAfter := time.After(25 * time.Second)
|
||||
for {
|
||||
select {
|
||||
case wEvent, ok := <-eventChan:
|
||||
if !ok {
|
||||
eventChan = nil
|
||||
continue
|
||||
}
|
||||
|
||||
switch wEvent.Type {
|
||||
case kubernetes.EventAdded:
|
||||
logger.Log.Debugf("Watching API Server pod loop, added")
|
||||
case kubernetes.EventDeleted:
|
||||
logger.Log.Infof("%s removed", kubernetes.ApiServerPodName)
|
||||
cancel()
|
||||
return
|
||||
case kubernetes.EventModified:
|
||||
modifiedPod, err := wEvent.ToPod()
|
||||
if err != nil {
|
||||
logger.Log.Errorf(uiUtils.Error, err)
|
||||
cancel()
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Log.Debugf("Watching API Server pod loop, modified: %v", modifiedPod.Status.Phase)
|
||||
|
||||
if modifiedPod.Status.Phase == core.PodPending {
|
||||
if modifiedPod.Status.Conditions[0].Type == core.PodScheduled && modifiedPod.Status.Conditions[0].Status != core.ConditionTrue {
|
||||
logger.Log.Debugf("Wasn't able to deploy the API server. Reason: \"%s\"", modifiedPod.Status.Conditions[0].Message)
|
||||
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Wasn't able to deploy the API server, for more info check logs at %s", fsUtils.GetLogFilePath()))
|
||||
cancel()
|
||||
break
|
||||
}
|
||||
|
||||
if len(modifiedPod.Status.ContainerStatuses) > 0 && modifiedPod.Status.ContainerStatuses[0].State.Waiting != nil && modifiedPod.Status.ContainerStatuses[0].State.Waiting.Reason == "ErrImagePull" {
|
||||
logger.Log.Debugf("Wasn't able to deploy the API server. (ErrImagePull) Reason: \"%s\"", modifiedPod.Status.ContainerStatuses[0].State.Waiting.Message)
|
||||
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Wasn't able to deploy the API server: failed to pull the image, for more info check logs at %v", fsUtils.GetLogFilePath()))
|
||||
cancel()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if modifiedPod.Status.Phase == core.PodRunning && !isPodReady {
|
||||
isPodReady = true
|
||||
go 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()
|
||||
break
|
||||
}
|
||||
|
||||
logger.Log.Infof("Mizu is available at %s", url)
|
||||
if !config.Config.HeadlessMode {
|
||||
uiUtils.OpenBrowser(url)
|
||||
}
|
||||
if err := apiProvider.ReportTappedPods(state.tapperSyncer.CurrentlyTappedPods); err != nil {
|
||||
logger.Log.Debugf("[Error] failed update tapped pods %v", err)
|
||||
}
|
||||
}
|
||||
case kubernetes.EventBookmark:
|
||||
break
|
||||
case kubernetes.EventError:
|
||||
break
|
||||
}
|
||||
case err, ok := <-errorChan:
|
||||
if !ok {
|
||||
errorChan = nil
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Log.Errorf("[ERROR] Agent creation, watching %v namespace, error: %v", config.Config.MizuResourcesNamespace, err)
|
||||
cancel()
|
||||
|
||||
case <-timeAfter:
|
||||
if !isPodReady {
|
||||
logger.Log.Errorf(uiUtils.Error, "Mizu API server was not ready in time")
|
||||
cancel()
|
||||
}
|
||||
case <-ctx.Done():
|
||||
logger.Log.Debugf("Watching API Server pod loop, ctx done")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func watchTapperPod(ctx context.Context, kubernetesProvider *kubernetes.Provider, cancel context.CancelFunc) {
|
||||
podExactRegex := regexp.MustCompile(fmt.Sprintf("^%s.*", kubernetes.TapperDaemonSetName))
|
||||
podWatchHelper := kubernetes.NewPodWatchHelper(kubernetesProvider, podExactRegex)
|
||||
eventChan, errorChan := kubernetes.FilteredWatch(ctx, podWatchHelper, []string{config.Config.MizuResourcesNamespace}, podWatchHelper)
|
||||
|
||||
for {
|
||||
select {
|
||||
case wEvent, ok := <-eventChan:
|
||||
if !ok {
|
||||
eventChan = nil
|
||||
continue
|
||||
}
|
||||
|
||||
pod, err := wEvent.ToPod()
|
||||
if err != nil {
|
||||
logger.Log.Errorf(uiUtils.Error, err)
|
||||
cancel()
|
||||
continue
|
||||
}
|
||||
|
||||
switch wEvent.Type {
|
||||
case kubernetes.EventAdded:
|
||||
logger.Log.Debugf("Tapper is created [%s]", pod.Name)
|
||||
case kubernetes.EventDeleted:
|
||||
logger.Log.Debugf("Tapper is removed [%s]", pod.Name)
|
||||
case kubernetes.EventModified:
|
||||
if pod.Status.Phase == core.PodPending && pod.Status.Conditions[0].Type == core.PodScheduled && pod.Status.Conditions[0].Status != core.ConditionTrue {
|
||||
logger.Log.Infof(uiUtils.Red, fmt.Sprintf("Wasn't able to deploy the tapper %s. Reason: \"%s\"", pod.Name, pod.Status.Conditions[0].Message))
|
||||
cancel()
|
||||
continue
|
||||
}
|
||||
|
||||
podStatus := pod.Status
|
||||
|
||||
if podStatus.Phase == core.PodRunning {
|
||||
state := podStatus.ContainerStatuses[0].State
|
||||
if state.Terminated != nil {
|
||||
switch state.Terminated.Reason {
|
||||
case "OOMKilled":
|
||||
logger.Log.Infof(uiUtils.Red, fmt.Sprintf("Tapper %s was terminated (reason: OOMKilled). You should consider increasing machine resources.", pod.Name))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.Log.Debugf("Tapper %s is %s", pod.Name, strings.ToLower(string(podStatus.Phase)))
|
||||
case kubernetes.EventBookmark:
|
||||
break
|
||||
case kubernetes.EventError:
|
||||
break
|
||||
}
|
||||
case err, ok := <-errorChan:
|
||||
if !ok {
|
||||
errorChan = nil
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Log.Errorf("[Error] Error in mizu tapper pod watch, err: %v", err)
|
||||
cancel()
|
||||
|
||||
case <-ctx.Done():
|
||||
logger.Log.Debugf("Watching tapper pod loop, ctx done")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func watchMizuEvents(ctx context.Context, kubernetesProvider *kubernetes.Provider, cancel context.CancelFunc, startTime time.Time) {
|
||||
// Round down because k8s CreationTimestamp is given in 1 sec resolution.
|
||||
startTime = startTime.Truncate(time.Second)
|
||||
|
||||
mizuResourceRegex := regexp.MustCompile(fmt.Sprintf("^%s.*", kubernetes.MizuResourcesPrefix))
|
||||
eventWatchHelper := kubernetes.NewEventWatchHelper(kubernetesProvider, mizuResourceRegex)
|
||||
podExactRegex := regexp.MustCompile(fmt.Sprintf("^%s", kubernetes.ApiServerPodName))
|
||||
eventWatchHelper := kubernetes.NewEventWatchHelper(kubernetesProvider, podExactRegex, "pod")
|
||||
eventChan, errorChan := kubernetes.FilteredWatch(ctx, eventWatchHelper, []string{config.Config.MizuResourcesNamespace}, eventWatchHelper)
|
||||
|
||||
for {
|
||||
select {
|
||||
case wEvent, ok := <-eventChan:
|
||||
@@ -732,16 +566,46 @@ func watchMizuEvents(ctx context.Context, kubernetesProvider *kubernetes.Provide
|
||||
|
||||
event, err := wEvent.ToEvent()
|
||||
if err != nil {
|
||||
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("error parsing Mizu resource event: %+v", err))
|
||||
cancel()
|
||||
logger.Log.Errorf(fmt.Sprintf("Error parsing Mizu resource event: %+v", err))
|
||||
}
|
||||
|
||||
if startTime.After(event.CreationTimestamp.Time) {
|
||||
if state.startTime.After(event.CreationTimestamp.Time) {
|
||||
continue
|
||||
}
|
||||
|
||||
if event.Type == core.EventTypeWarning {
|
||||
logger.Log.Warningf(uiUtils.Warning, fmt.Sprintf("Resource %s in state %s - %s", event.Regarding.Name, event.Reason, event.Note))
|
||||
logger.Log.Debugf(
|
||||
fmt.Sprintf("Watching API server events loop, event %s, time: %v, resource: %s (%s), reason: %s, note: %s",
|
||||
event.Name,
|
||||
event.CreationTimestamp.Time,
|
||||
event.Regarding.Name,
|
||||
event.Regarding.Kind,
|
||||
event.Reason,
|
||||
event.Note))
|
||||
|
||||
switch event.Reason {
|
||||
case "Started":
|
||||
go 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()
|
||||
break
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
logger.Log.Infof("Mizu is available at %s", url)
|
||||
if !config.Config.HeadlessMode {
|
||||
uiUtils.OpenBrowser(url)
|
||||
}
|
||||
case "FailedScheduling", "Failed", "Killing":
|
||||
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Mizu API Server status: %s - %s", event.Reason, event.Note))
|
||||
cancel()
|
||||
break
|
||||
}
|
||||
case err, ok := <-errorChan:
|
||||
if !ok {
|
||||
@@ -749,11 +613,9 @@ func watchMizuEvents(ctx context.Context, kubernetesProvider *kubernetes.Provide
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Log.Errorf("error in watch mizu resource events loop: %+v", err)
|
||||
cancel()
|
||||
|
||||
logger.Log.Errorf("Watching API server events loop, error: %+v", err)
|
||||
case <-ctx.Done():
|
||||
logger.Log.Debugf("watching Mizu resource events loop, ctx done")
|
||||
logger.Log.Debugf("Watching API server events loop, ctx done")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,12 @@ var (
|
||||
GitCommitHash = "" // this var is overridden using ldflags in makefile when building
|
||||
BuildTimestamp = "" // this var is overridden using ldflags in makefile when building
|
||||
RBACVersion = "v1"
|
||||
Platform = ""
|
||||
DaemonModePersistentVolumeSizeBufferBytes = int64(500 * 1000 * 1000) //500mb
|
||||
)
|
||||
|
||||
const DEVENVVAR = "MIZU_DISABLE_TELEMTRY"
|
||||
|
||||
func GetMizuFolderPath() string {
|
||||
home, homeDirErr := os.UserHomeDir()
|
||||
if homeDirErr != nil {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -39,6 +40,10 @@ func CheckVersionCompatibility(apiServerProvider *apiserver.Provider) (bool, err
|
||||
}
|
||||
|
||||
func CheckNewerVersion(versionChan chan string) {
|
||||
if _, present := os.LookupEnv(mizu.DEVENVVAR); present {
|
||||
versionChan <- ""
|
||||
return
|
||||
}
|
||||
logger.Log.Debugf("Checking for newer version...")
|
||||
start := time.Now()
|
||||
client := github.NewClient(nil)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/denisbrodbeck/machineid"
|
||||
"github.com/up9inc/mizu/cli/apiserver"
|
||||
@@ -62,6 +63,9 @@ func ReportAPICalls(apiProvider *apiserver.Provider) {
|
||||
}
|
||||
|
||||
func shouldRunTelemetry() bool {
|
||||
if _, present := os.LookupEnv(mizu.DEVENVVAR); present {
|
||||
return false
|
||||
}
|
||||
if !config.Config.Telemetry {
|
||||
return false
|
||||
}
|
||||
@@ -79,6 +83,7 @@ func sendTelemetry(telemetryType string, argsMap map[string]interface{}) error {
|
||||
argsMap["buildTimestamp"] = mizu.BuildTimestamp
|
||||
argsMap["branch"] = mizu.Branch
|
||||
argsMap["version"] = mizu.SemVer
|
||||
argsMap["Platform"] = mizu.Platform
|
||||
|
||||
if machineId, err := machineid.ProtectedID("mizu"); err == nil {
|
||||
argsMap["machineId"] = machineId
|
||||
|
||||
@@ -37,8 +37,8 @@ COPY agent .
|
||||
RUN go build -gcflags="all=-N -l" -o mizuagent .
|
||||
|
||||
# Download Basenine executable, verify the sha1sum and move it to a directory in $PATH
|
||||
ADD https://github.com/up9inc/basenine/releases/download/v0.2.11/basenine_linux_amd64 ./basenine_linux_amd64
|
||||
ADD https://github.com/up9inc/basenine/releases/download/v0.2.11/basenine_linux_amd64.sha256 ./basenine_linux_amd64.sha256
|
||||
ADD https://github.com/up9inc/basenine/releases/download/v0.2.17/basenine_linux_amd64 ./basenine_linux_amd64
|
||||
ADD https://github.com/up9inc/basenine/releases/download/v0.2.17/basenine_linux_amd64.sha256 ./basenine_linux_amd64.sha256
|
||||
RUN shasum -a 256 -c basenine_linux_amd64.sha256
|
||||
RUN chmod +x ./basenine_linux_amd64
|
||||
|
||||
|
||||
91
docs/CONFIGURATION.md
Normal file
91
docs/CONFIGURATION.md
Normal file
@@ -0,0 +1,91 @@
|
||||

|
||||
# Configuration options for Mizu
|
||||
|
||||
Mizu has many configuration options and flags that affect its behavior. Their values can be modified via command-line interface or via configuration file.
|
||||
|
||||
The list below covers most useful configuration options.
|
||||
|
||||
### Config file
|
||||
Mizu behaviour can be modified via YAML configuration file located at `$HOME/.mizu/config.yaml`.
|
||||
|
||||
Default values for the file can be viewed via `mizu config` command.
|
||||
|
||||
### Applying config options via command line
|
||||
To apply any configuration option via command line, use `--set` following by config option name and value, like in the following example:
|
||||
|
||||
```
|
||||
mizu tap --set tap.dry-run=true
|
||||
```
|
||||
|
||||
Please make sure to use full option name (`tap.dry-run` as opposed to `dry-run` only), incl. section (`tap`, in this example)
|
||||
|
||||
## General section
|
||||
|
||||
* `agent-image` - full path to Mizu container image, in format `full.path.to/your/image:tag`. Default value is set at compilation time to `gcr.io/up9-docker-hub/mizu/<branch>:<version>`
|
||||
|
||||
* `dump-logs` - if set to `true`, saves log files for all Mizu components (tapper, api-server, CLI) in a zip file under `$HOME/.mizu`. Default value is `false`
|
||||
|
||||
* `image-pull-policy` - container image pull policy for Kubernetes, default value `Always`. Other accepted values are `Never` or `IfNotExist`. Please mind the implications when changing this.
|
||||
|
||||
* `kube-config-path` - path to alternative kubeconfig file to use for all interactions with Kubernetes cluster. By default - `$HOME/.kubeconfig`
|
||||
|
||||
* `mizu-resources-namespace` - Kubernetes namespace where all Mizu-related resources are created. Default value `mizu`
|
||||
|
||||
* `telemetry` - report anonymous usage statistics. Default value `true`
|
||||
|
||||
## section `tap`
|
||||
* `namespaces` - list of namespace names, in which pods are tapped. Default value is empty, meaning only pods in the current namespace are tapped. Typically supplied as command line options.
|
||||
|
||||
* `all-namespaces` - special flag indicating whether Mizu should search and tap pods, matching the regex, in all namespaces. Default is `false`. Please use with caution, tapping too many pods can affect resource consumption.
|
||||
|
||||
* `daemon` - instructs Mizu whether to run daemon mode (where CLI command exits after launch, and tapper & api-server pods in Kubernetes continue to run without controlling CLI). Typically supplied as command-line option `--daemon`. Default valie is `false`
|
||||
|
||||
* `dry-run` - if true, Mizu will print list of pods matching the supplied (or default) regex and exit without actually tapping the traffic. Default value is `false`. Typically supplied as command-line option `--dry-run`
|
||||
|
||||
* `proxy-host` - IP address on which proxy to Mizu API service is launched; should be accessible at `proxy-host:gui-port`. Default value is `127.0.0.1`
|
||||
|
||||
* `gui-port` - port on which Mizu GUI is accessible, default value is `8899` (stands for `8899/tcp`)
|
||||
|
||||
* `regex` - regular expression used to match pods to tap, when no regex is given in the command line; default value is `.*`, which means `mizu tap` with no additional arguments is runnining as `mizu tap .*` (i.e. tap all pods found in current workspace)
|
||||
|
||||
* `no-redact` - instructs Mizu whether to redact certain sensitive fields in the collected traffic. Default value is `false`, i.e. Mizu will replace sentitive data values with *REDACTED* placeholder.
|
||||
|
||||
* `ignored-user-agents` - array of strings, describing HTTP *User-Agent* header values to be ignored. Useful to ignore Kubernetes healthcheck and other similar noisy periodic probes. Default value is empty.
|
||||
|
||||
* `max-entries-db-size` - maximal size of traffic stored locally in the `mizu-api-server` pod. When this size is reached, older traffic is overwritten with new entries. Default value is `200MB`
|
||||
|
||||
|
||||
### section `tap.api-server-resources`
|
||||
Kubernetes request and limit values for the `mizu-api-server` pod.
|
||||
Parameters and their default values are same as used natively in Kubernetes pods:
|
||||
|
||||
```
|
||||
cpu-limit: 750m
|
||||
memory-limit: 1Gi
|
||||
cpu-requests: 50m
|
||||
memory-requests: 50Mi
|
||||
```
|
||||
|
||||
### section `tap.tapper-resources`
|
||||
Kubernetes request and limit values for the `mizu-tapper` pods (launched via daemonset).
|
||||
Parameters and their default values are same as used natively in Kubernetes pods:
|
||||
|
||||
```
|
||||
cpu-limit: 750m
|
||||
memory-limit: 1Gi
|
||||
cpu-requests: 50m
|
||||
memory-requests: 50Mi
|
||||
```
|
||||
|
||||
|
||||
--
|
||||
|
||||
* `analsys` - enables advanced analysis of collected traffic in the UP9 coud platform. Default value is `false`
|
||||
|
||||
* `upload-interval` - in the *analysis* mode, push traffic to UP9 cloud every `upload-interval` seconds. Default value is `10` seconds
|
||||
|
||||
* `ask-upload-confirmation` - request user confirmation when uploading tapped data to UP9 cloud
|
||||
|
||||
|
||||
## section `version`
|
||||
* `debug`- print additional version and build information when `mizu version` command is invoked. Default value is `false`.
|
||||
@@ -3,6 +3,7 @@ package kubernetes
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
@@ -10,13 +11,15 @@ import (
|
||||
|
||||
type EventWatchHelper struct {
|
||||
kubernetesProvider *Provider
|
||||
NameRegexFilter *regexp.Regexp
|
||||
NameRegexFilter *regexp.Regexp
|
||||
Kind string
|
||||
}
|
||||
|
||||
func NewEventWatchHelper(kubernetesProvider *Provider, NameRegexFilter *regexp.Regexp) *EventWatchHelper {
|
||||
func NewEventWatchHelper(kubernetesProvider *Provider, NameRegexFilter *regexp.Regexp, kind string) *EventWatchHelper {
|
||||
return &EventWatchHelper{
|
||||
kubernetesProvider: kubernetesProvider,
|
||||
NameRegexFilter: NameRegexFilter,
|
||||
NameRegexFilter: NameRegexFilter,
|
||||
Kind: kind,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +34,10 @@ func (wh *EventWatchHelper) Filter(wEvent *WatchEvent) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if strings.ToLower(event.Regarding.Kind) != strings.ToLower(wh.Kind) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -23,13 +23,15 @@ type TappedPodChangeEvent struct {
|
||||
|
||||
// MizuTapperSyncer uses a k8s pod watch to update tapper daemonsets when targeted pods are removed or created
|
||||
type MizuTapperSyncer struct {
|
||||
context context.Context
|
||||
CurrentlyTappedPods []core.Pod
|
||||
config TapperSyncerConfig
|
||||
kubernetesProvider *Provider
|
||||
TapPodChangesOut chan TappedPodChangeEvent
|
||||
ErrorOut chan K8sTapManagerError
|
||||
nodeToTappedPodIPMap map[string][]string
|
||||
startTime time.Time
|
||||
context context.Context
|
||||
CurrentlyTappedPods []core.Pod
|
||||
config TapperSyncerConfig
|
||||
kubernetesProvider *Provider
|
||||
TapPodChangesOut chan TappedPodChangeEvent
|
||||
TapperStatusChangedOut chan shared.TapperStatus
|
||||
ErrorOut chan K8sTapManagerError
|
||||
nodeToTappedPodIPMap map[string][]string
|
||||
}
|
||||
|
||||
type TapperSyncerConfig struct {
|
||||
@@ -46,14 +48,16 @@ type TapperSyncerConfig struct {
|
||||
Istio bool
|
||||
}
|
||||
|
||||
func CreateAndStartMizuTapperSyncer(ctx context.Context, kubernetesProvider *Provider, config TapperSyncerConfig) (*MizuTapperSyncer, error) {
|
||||
func CreateAndStartMizuTapperSyncer(ctx context.Context, kubernetesProvider *Provider, config TapperSyncerConfig, startTime time.Time) (*MizuTapperSyncer, error) {
|
||||
syncer := &MizuTapperSyncer{
|
||||
context: ctx,
|
||||
CurrentlyTappedPods: make([]core.Pod, 0),
|
||||
config: config,
|
||||
kubernetesProvider: kubernetesProvider,
|
||||
TapPodChangesOut: make(chan TappedPodChangeEvent, 100),
|
||||
ErrorOut: make(chan K8sTapManagerError, 100),
|
||||
startTime: startTime.Truncate(time.Second), // Round down because k8s CreationTimestamp is given in 1 sec resolution.
|
||||
context: ctx,
|
||||
CurrentlyTappedPods: make([]core.Pod, 0),
|
||||
config: config,
|
||||
kubernetesProvider: kubernetesProvider,
|
||||
TapPodChangesOut: make(chan TappedPodChangeEvent, 100),
|
||||
TapperStatusChangedOut: make(chan shared.TapperStatus, 100),
|
||||
ErrorOut: make(chan K8sTapManagerError, 100),
|
||||
}
|
||||
|
||||
if err, _ := syncer.updateCurrentlyTappedPods(); err != nil {
|
||||
@@ -65,9 +69,72 @@ func CreateAndStartMizuTapperSyncer(ctx context.Context, kubernetesProvider *Pro
|
||||
}
|
||||
|
||||
go syncer.watchPodsForTapping()
|
||||
go syncer.watchTapperEvents()
|
||||
return syncer, nil
|
||||
}
|
||||
|
||||
func (tapperSyncer *MizuTapperSyncer) watchTapperEvents() {
|
||||
mizuResourceRegex := regexp.MustCompile(fmt.Sprintf("^%s.*", TapperPodName))
|
||||
eventWatchHelper := NewEventWatchHelper(tapperSyncer.kubernetesProvider, mizuResourceRegex, "pod")
|
||||
eventChan, errorChan := FilteredWatch(tapperSyncer.context, eventWatchHelper, []string{tapperSyncer.config.MizuResourcesNamespace}, eventWatchHelper)
|
||||
|
||||
for {
|
||||
select {
|
||||
case wEvent, ok := <-eventChan:
|
||||
if !ok {
|
||||
eventChan = nil
|
||||
continue
|
||||
}
|
||||
|
||||
event, err := wEvent.ToEvent()
|
||||
if err != nil {
|
||||
logger.Log.Errorf(fmt.Sprintf("Error parsing Mizu resource event: %+v", err))
|
||||
}
|
||||
|
||||
if tapperSyncer.startTime.After(event.CreationTimestamp.Time) {
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Log.Debugf(
|
||||
fmt.Sprintf("Watching tapper events loop, event %s, time: %v, resource: %s (%s), reason: %s, note: %s",
|
||||
event.Name,
|
||||
event.CreationTimestamp.Time,
|
||||
event.Regarding.Name,
|
||||
event.Regarding.Kind,
|
||||
event.Reason,
|
||||
event.Note))
|
||||
|
||||
pod, err1 := tapperSyncer.kubernetesProvider.GetPod(tapperSyncer.context, tapperSyncer.config.MizuResourcesNamespace, event.Regarding.Name)
|
||||
if err1 != nil {
|
||||
logger.Log.Debugf(fmt.Sprintf("Failed to get tapper pod %s", event.Regarding.Name))
|
||||
continue
|
||||
}
|
||||
|
||||
nodeName := ""
|
||||
if event.Reason != "FailedScheduling" {
|
||||
nodeName = pod.Spec.NodeName
|
||||
} else {
|
||||
nodeName = pod.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms[0].MatchFields[0].Values[0]
|
||||
}
|
||||
|
||||
taperStatus := shared.TapperStatus{TapperName: pod.Name, NodeName: nodeName, Status: event.Reason}
|
||||
tapperSyncer.TapperStatusChangedOut <- taperStatus
|
||||
|
||||
case err, ok := <-errorChan:
|
||||
if !ok {
|
||||
errorChan = nil
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Log.Errorf("Watching tapper events loop, error: %+v", err)
|
||||
|
||||
case <-tapperSyncer.context.Done():
|
||||
logger.Log.Debugf("Watching tapper events loop, ctx done")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (tapperSyncer *MizuTapperSyncer) watchPodsForTapping() {
|
||||
podWatchHelper := NewPodWatchHelper(tapperSyncer.kubernetesProvider, &tapperSyncer.config.PodFilterRegex)
|
||||
eventChan, errorChan := FilteredWatch(tapperSyncer.context, podWatchHelper, tapperSyncer.config.TargetNamespaces, podWatchHelper)
|
||||
@@ -108,7 +175,6 @@ func (tapperSyncer *MizuTapperSyncer) watchPodsForTapping() {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
switch wEvent.Type {
|
||||
case EventAdded:
|
||||
logger.Log.Debugf("Added matching pod %s, ns: %s", pod.Name, pod.Namespace)
|
||||
|
||||
@@ -351,8 +351,8 @@ func (provider *Provider) CreateService(ctx context.Context, namespace string, s
|
||||
}
|
||||
|
||||
func (provider *Provider) DoesServicesExist(ctx context.Context, namespace string, name string) (bool, error) {
|
||||
resource, err := provider.clientSet.CoreV1().Services(namespace).Get(ctx, name, metav1.GetOptions{})
|
||||
return provider.doesResourceExist(resource, err)
|
||||
serviceResource, err := provider.clientSet.CoreV1().Services(namespace).Get(ctx, name, metav1.GetOptions{})
|
||||
return provider.doesResourceExist(serviceResource, err)
|
||||
}
|
||||
|
||||
func (provider *Provider) doesResourceExist(resource interface{}, err error) (bool, error) {
|
||||
@@ -642,7 +642,7 @@ func (provider *Provider) ApplyMizuTapperDaemonSet(ctx context.Context, namespac
|
||||
"--api-server-address", fmt.Sprintf("ws://%s/wsTapper", apiServerPodIp),
|
||||
"--nodefrag",
|
||||
}
|
||||
|
||||
|
||||
if istio {
|
||||
mizuCmd = append(mizuCmd, "--procfs", procfsMountPath, "--istio")
|
||||
}
|
||||
@@ -651,7 +651,17 @@ func (provider *Provider) ApplyMizuTapperDaemonSet(ctx context.Context, namespac
|
||||
agentContainer.WithName(tapperPodName)
|
||||
agentContainer.WithImage(podImage)
|
||||
agentContainer.WithImagePullPolicy(imagePullPolicy)
|
||||
agentContainer.WithSecurityContext(applyconfcore.SecurityContext().WithPrivileged(true))
|
||||
|
||||
caps := applyconfcore.Capabilities().WithDrop("ALL").WithAdd("NET_RAW").WithAdd("NET_ADMIN")
|
||||
|
||||
if istio {
|
||||
caps = caps.WithAdd("SYS_ADMIN") // for reading /proc/PID/net/ns
|
||||
caps = caps.WithAdd("SYS_PTRACE") // for setting netns to other process
|
||||
caps = caps.WithAdd("DAC_OVERRIDE") // for reading /proc/PID/environ
|
||||
}
|
||||
|
||||
agentContainer.WithSecurityContext(applyconfcore.SecurityContext().WithCapabilities(caps))
|
||||
|
||||
agentContainer.WithCommand(mizuCmd...)
|
||||
agentContainer.WithEnv(
|
||||
applyconfcore.EnvVar().WithName(shared.LogLevelEnvVar).WithValue(logLevel.String()),
|
||||
@@ -770,10 +780,10 @@ func (provider *Provider) ApplyMizuTapperDaemonSet(ctx context.Context, namespac
|
||||
return err
|
||||
}
|
||||
|
||||
func (provider *Provider) ListAllPodsMatchingRegex(ctx context.Context, regex *regexp.Regexp, namespaces []string) ([]core.Pod, error) {
|
||||
func (provider *Provider) listPodsImpl(ctx context.Context, regex *regexp.Regexp, namespaces []string, listOptions metav1.ListOptions) ([]core.Pod, error) {
|
||||
var pods []core.Pod
|
||||
for _, namespace := range namespaces {
|
||||
namespacePods, err := provider.clientSet.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{})
|
||||
namespacePods, err := provider.clientSet.CoreV1().Pods(namespace).List(ctx, listOptions)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get pods in ns: [%s], %w", namespace, err)
|
||||
}
|
||||
@@ -790,6 +800,14 @@ func (provider *Provider) ListAllPodsMatchingRegex(ctx context.Context, regex *r
|
||||
return matchingPods, nil
|
||||
}
|
||||
|
||||
func (provider *Provider) ListAllPodsMatchingRegex(ctx context.Context, regex *regexp.Regexp, namespaces []string) ([]core.Pod, error) {
|
||||
return provider.listPodsImpl(ctx, regex, namespaces, metav1.ListOptions{})
|
||||
}
|
||||
|
||||
func (provider *Provider) GetPod(ctx context.Context, namespaces string, podName string) (*core.Pod, error) {
|
||||
return provider.clientSet.CoreV1().Pods(namespaces).Get(ctx, podName, metav1.GetOptions{})
|
||||
}
|
||||
|
||||
func (provider *Provider) ListAllRunningPodsMatchingRegex(ctx context.Context, regex *regexp.Regexp, namespaces []string) ([]core.Pod, error) {
|
||||
pods, err := provider.ListAllPodsMatchingRegex(ctx, regex, namespaces)
|
||||
if err != nil {
|
||||
|
||||
@@ -57,11 +57,10 @@ func getMissingPods(pods1 []core.Pod, pods2 []core.Pod) []core.Pod {
|
||||
return missingPods
|
||||
}
|
||||
|
||||
|
||||
func GetPodInfosForPods(pods []core.Pod) []shared.PodInfo {
|
||||
podInfos := make([]shared.PodInfo, 0)
|
||||
for _, pod := range pods {
|
||||
podInfos = append(podInfos, shared.PodInfo{Name: pod.Name, Namespace: pod.Namespace})
|
||||
podInfos = append(podInfos, shared.PodInfo{Name: pod.Name, Namespace: pod.Namespace, NodeName: pod.Spec.NodeName})
|
||||
}
|
||||
return podInfos
|
||||
}
|
||||
|
||||
@@ -67,6 +67,12 @@ type WebSocketStatusMessage struct {
|
||||
TappingStatus TapStatus `json:"tappingStatus"`
|
||||
}
|
||||
|
||||
type TapperStatus struct {
|
||||
TapperName string `json:"tapperName"`
|
||||
NodeName string `json:"nodeName"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type TapStatus struct {
|
||||
Pods []PodInfo `json:"pods"`
|
||||
TLSLinks []TLSLinkInfo `json:"tlsLinks"`
|
||||
@@ -75,6 +81,7 @@ type TapStatus struct {
|
||||
type PodInfo struct {
|
||||
Namespace string `json:"namespace"`
|
||||
Name string `json:"name"`
|
||||
NodeName string `json:"nodeName"`
|
||||
}
|
||||
|
||||
type TLSLinkInfo struct {
|
||||
@@ -110,8 +117,9 @@ func CreateWebSocketMessageTypeAnalyzeStatus(analyzeStatus AnalyzeStatus) WebSoc
|
||||
}
|
||||
|
||||
type HealthResponse struct {
|
||||
TapStatus TapStatus `json:"tapStatus"`
|
||||
TappersCount int `json:"tappersCount"`
|
||||
TapStatus TapStatus `json:"tapStatus"`
|
||||
TappersCount int `json:"tappersCount"`
|
||||
TappersStatus []TapperStatus `json:"tappersStatus"`
|
||||
}
|
||||
|
||||
type VersionResponse struct {
|
||||
|
||||
@@ -129,19 +129,10 @@ type MizuEntry struct {
|
||||
Response map[string]interface{} `json:"response"`
|
||||
Base *BaseEntryDetails `json:"base"`
|
||||
Summary string `json:"summary"`
|
||||
Url string `json:"url"`
|
||||
Method string `json:"method"`
|
||||
Status int `json:"status"`
|
||||
RequestSenderIp string `json:"requestSenderIp"`
|
||||
Service string `json:"service"`
|
||||
ElapsedTime int64 `json:"elapsedTime"`
|
||||
Path string `json:"path"`
|
||||
ResolvedSource string `json:"resolvedSource,omitempty"`
|
||||
ResolvedDestination string `json:"resolvedDestination,omitempty"`
|
||||
SourceIp string `json:"sourceIp,omitempty"`
|
||||
DestinationIp string `json:"destinationIp,omitempty"`
|
||||
SourcePort string `json:"sourcePort,omitempty"`
|
||||
DestinationPort string `json:"destinationPort,omitempty"`
|
||||
IsOutgoing bool `json:"isOutgoing,omitempty"`
|
||||
ContractStatus ContractStatus `json:"contractStatus,omitempty"`
|
||||
ContractRequestReason string `json:"contractRequestReason,omitempty"`
|
||||
@@ -160,24 +151,20 @@ type MizuEntryWrapper struct {
|
||||
}
|
||||
|
||||
type BaseEntryDetails struct {
|
||||
Id uint `json:"id"`
|
||||
Protocol Protocol `json:"protocol,omitempty"`
|
||||
Url string `json:"url,omitempty"`
|
||||
RequestSenderIp string `json:"requestSenderIp,omitempty"`
|
||||
Service string `json:"service,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Summary string `json:"summary,omitempty"`
|
||||
StatusCode int `json:"statusCode"`
|
||||
Method string `json:"method,omitempty"`
|
||||
Timestamp int64 `json:"timestamp,omitempty"`
|
||||
SourceIp string `json:"sourceIp,omitempty"`
|
||||
DestinationIp string `json:"destinationIp,omitempty"`
|
||||
SourcePort string `json:"sourcePort,omitempty"`
|
||||
DestinationPort string `json:"destinationPort,omitempty"`
|
||||
IsOutgoing bool `json:"isOutgoing,omitempty"`
|
||||
Latency int64 `json:"latency"`
|
||||
Rules ApplicableRules `json:"rules,omitempty"`
|
||||
ContractStatus ContractStatus `json:"contractStatus"`
|
||||
Id uint `json:"id"`
|
||||
Protocol Protocol `json:"protocol,omitempty"`
|
||||
Url string `json:"url,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Summary string `json:"summary,omitempty"`
|
||||
StatusCode int `json:"statusCode"`
|
||||
Method string `json:"method,omitempty"`
|
||||
Timestamp int64 `json:"timestamp,omitempty"`
|
||||
Source *TCP `json:"src"`
|
||||
Destination *TCP `json:"dst"`
|
||||
IsOutgoing bool `json:"isOutgoing,omitempty"`
|
||||
Latency int64 `json:"latency"`
|
||||
Rules ApplicableRules `json:"rules,omitempty"`
|
||||
ContractStatus ContractStatus `json:"contractStatus"`
|
||||
}
|
||||
|
||||
type ApplicableRules struct {
|
||||
@@ -202,18 +189,13 @@ type DataUnmarshaler interface {
|
||||
func (bed *BaseEntryDetails) UnmarshalData(entry *MizuEntry) error {
|
||||
bed.Protocol = entry.Protocol
|
||||
bed.Id = entry.Id
|
||||
bed.Url = entry.Url
|
||||
bed.RequestSenderIp = entry.RequestSenderIp
|
||||
bed.Service = entry.Service
|
||||
bed.Path = entry.Path
|
||||
bed.Summary = entry.Path
|
||||
bed.Summary = entry.Summary
|
||||
bed.StatusCode = entry.Status
|
||||
bed.Method = entry.Method
|
||||
bed.Timestamp = entry.Timestamp
|
||||
bed.SourceIp = entry.SourceIp
|
||||
bed.DestinationIp = entry.DestinationIp
|
||||
bed.SourcePort = entry.SourcePort
|
||||
bed.DestinationPort = entry.DestinationPort
|
||||
bed.Source = entry.Source
|
||||
bed.Destination = entry.Destination
|
||||
bed.IsOutgoing = entry.IsOutgoing
|
||||
bed.Latency = entry.ElapsedTime
|
||||
bed.ContractStatus = entry.ContractStatus
|
||||
@@ -271,7 +253,6 @@ func (h HTTPPayload) MarshalJSON() ([]byte, error) {
|
||||
}
|
||||
return json.Marshal(&HTTPWrapper{
|
||||
Method: harRequest.Method,
|
||||
Url: "",
|
||||
Details: harRequest,
|
||||
RawRequest: &HTTPRequestWrapper{Request: h.Data.(*http.Request)},
|
||||
})
|
||||
|
||||
@@ -226,12 +226,6 @@ func (d dissecting) Dissect(b *bufio.Reader, isClient bool, tcpID *api.TcpID, co
|
||||
func (d dissecting) Analyze(item *api.OutputChannelItem, resolvedSource string, resolvedDestination string) *api.MizuEntry {
|
||||
request := item.Pair.Request.Payload.(map[string]interface{})
|
||||
reqDetails := request["details"].(map[string]interface{})
|
||||
service := "amqp"
|
||||
if resolvedDestination != "" {
|
||||
service = resolvedDestination
|
||||
} else if resolvedSource != "" {
|
||||
service = resolvedSource
|
||||
}
|
||||
|
||||
summary := ""
|
||||
switch request["method"] {
|
||||
@@ -279,45 +273,31 @@ func (d dissecting) Analyze(item *api.OutputChannelItem, resolvedSource string,
|
||||
IP: item.ConnectionInfo.ServerIP,
|
||||
Port: item.ConnectionInfo.ServerPort,
|
||||
},
|
||||
Outgoing: item.ConnectionInfo.IsOutgoing,
|
||||
Request: reqDetails,
|
||||
Url: fmt.Sprintf("%s%s", service, summary),
|
||||
Method: request["method"].(string),
|
||||
Status: 0,
|
||||
RequestSenderIp: item.ConnectionInfo.ClientIP,
|
||||
Service: service,
|
||||
Timestamp: item.Timestamp,
|
||||
StartTime: item.Pair.Request.CaptureTime,
|
||||
ElapsedTime: 0,
|
||||
Summary: summary,
|
||||
ResolvedSource: resolvedSource,
|
||||
ResolvedDestination: resolvedDestination,
|
||||
SourceIp: item.ConnectionInfo.ClientIP,
|
||||
DestinationIp: item.ConnectionInfo.ServerIP,
|
||||
SourcePort: item.ConnectionInfo.ClientPort,
|
||||
DestinationPort: item.ConnectionInfo.ServerPort,
|
||||
IsOutgoing: item.ConnectionInfo.IsOutgoing,
|
||||
Outgoing: item.ConnectionInfo.IsOutgoing,
|
||||
Request: reqDetails,
|
||||
Method: request["method"].(string),
|
||||
Status: 0,
|
||||
Timestamp: item.Timestamp,
|
||||
StartTime: item.Pair.Request.CaptureTime,
|
||||
ElapsedTime: 0,
|
||||
Summary: summary,
|
||||
IsOutgoing: item.ConnectionInfo.IsOutgoing,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (d dissecting) Summarize(entry *api.MizuEntry) *api.BaseEntryDetails {
|
||||
return &api.BaseEntryDetails{
|
||||
Id: entry.Id,
|
||||
Protocol: protocol,
|
||||
Url: entry.Url,
|
||||
RequestSenderIp: entry.RequestSenderIp,
|
||||
Service: entry.Service,
|
||||
Summary: entry.Summary,
|
||||
StatusCode: entry.Status,
|
||||
Method: entry.Method,
|
||||
Timestamp: entry.Timestamp,
|
||||
SourceIp: entry.SourceIp,
|
||||
DestinationIp: entry.DestinationIp,
|
||||
SourcePort: entry.SourcePort,
|
||||
DestinationPort: entry.DestinationPort,
|
||||
IsOutgoing: entry.IsOutgoing,
|
||||
Latency: entry.ElapsedTime,
|
||||
Id: entry.Id,
|
||||
Protocol: protocol,
|
||||
Summary: entry.Summary,
|
||||
StatusCode: entry.Status,
|
||||
Method: entry.Method,
|
||||
Timestamp: entry.Timestamp,
|
||||
Source: entry.Source,
|
||||
Destination: entry.Destination,
|
||||
IsOutgoing: entry.IsOutgoing,
|
||||
Latency: entry.ElapsedTime,
|
||||
Rules: api.ApplicableRules{
|
||||
Latency: 0,
|
||||
Status: false,
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/up9inc/mizu/tap/api"
|
||||
)
|
||||
@@ -34,12 +35,13 @@ func handleHTTP2Stream(http2Assembler *Http2Assembler, tcpID *api.TcpID, superTi
|
||||
switch messageHTTP1 := messageHTTP1.(type) {
|
||||
case http.Request:
|
||||
ident := fmt.Sprintf(
|
||||
"%s->%s %s->%s %d",
|
||||
"%s->%s %s->%s %d %s",
|
||||
tcpID.SrcIP,
|
||||
tcpID.DstIP,
|
||||
tcpID.SrcPort,
|
||||
tcpID.DstPort,
|
||||
streamID,
|
||||
"HTTP2",
|
||||
)
|
||||
item = reqResMatcher.registerRequest(ident, &messageHTTP1, superTimer.CaptureTime)
|
||||
if item != nil {
|
||||
@@ -53,12 +55,13 @@ func handleHTTP2Stream(http2Assembler *Http2Assembler, tcpID *api.TcpID, superTi
|
||||
}
|
||||
case http.Response:
|
||||
ident := fmt.Sprintf(
|
||||
"%s->%s %s->%s %d",
|
||||
"%s->%s %s->%s %d %s",
|
||||
tcpID.DstIP,
|
||||
tcpID.SrcIP,
|
||||
tcpID.DstPort,
|
||||
tcpID.SrcPort,
|
||||
streamID,
|
||||
"HTTP2",
|
||||
)
|
||||
item = reqResMatcher.registerResponse(ident, &messageHTTP1, superTimer.CaptureTime)
|
||||
if item != nil {
|
||||
@@ -84,23 +87,30 @@ func handleHTTP2Stream(http2Assembler *Http2Assembler, tcpID *api.TcpID, superTi
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleHTTP1ClientStream(b *bufio.Reader, tcpID *api.TcpID, counterPair *api.CounterPair, superTimer *api.SuperTimer, emitter api.Emitter, options *api.TrafficFilteringOptions) error {
|
||||
req, err := http.ReadRequest(b)
|
||||
func handleHTTP1ClientStream(b *bufio.Reader, tcpID *api.TcpID, counterPair *api.CounterPair, superTimer *api.SuperTimer, emitter api.Emitter, options *api.TrafficFilteringOptions) (switchingProtocolsHTTP2 bool, req *http.Request, err error) {
|
||||
req, err = http.ReadRequest(b)
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
counterPair.Request++
|
||||
|
||||
body, err := ioutil.ReadAll(req.Body)
|
||||
// Check HTTP2 upgrade - HTTP2 Over Cleartext (H2C)
|
||||
if strings.Contains(strings.ToLower(req.Header.Get("Connection")), "upgrade") && strings.ToLower(req.Header.Get("Upgrade")) == "h2c" {
|
||||
switchingProtocolsHTTP2 = true
|
||||
}
|
||||
|
||||
var body []byte
|
||||
body, err = ioutil.ReadAll(req.Body)
|
||||
req.Body = io.NopCloser(bytes.NewBuffer(body)) // rewind
|
||||
|
||||
ident := fmt.Sprintf(
|
||||
"%s->%s %s->%s %d",
|
||||
"%s->%s %s->%s %d %s",
|
||||
tcpID.SrcIP,
|
||||
tcpID.DstIP,
|
||||
tcpID.SrcPort,
|
||||
tcpID.DstPort,
|
||||
counterPair.Request,
|
||||
"HTTP1",
|
||||
)
|
||||
item := reqResMatcher.registerRequest(ident, req, superTimer.CaptureTime)
|
||||
if item != nil {
|
||||
@@ -113,26 +123,34 @@ func handleHTTP1ClientStream(b *bufio.Reader, tcpID *api.TcpID, counterPair *api
|
||||
}
|
||||
filterAndEmit(item, emitter, options)
|
||||
}
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
func handleHTTP1ServerStream(b *bufio.Reader, tcpID *api.TcpID, counterPair *api.CounterPair, superTimer *api.SuperTimer, emitter api.Emitter, options *api.TrafficFilteringOptions) error {
|
||||
res, err := http.ReadResponse(b, nil)
|
||||
func handleHTTP1ServerStream(b *bufio.Reader, tcpID *api.TcpID, counterPair *api.CounterPair, superTimer *api.SuperTimer, emitter api.Emitter, options *api.TrafficFilteringOptions) (switchingProtocolsHTTP2 bool, err error) {
|
||||
var res *http.Response
|
||||
res, err = http.ReadResponse(b, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
counterPair.Response++
|
||||
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
// Check HTTP2 upgrade - HTTP2 Over Cleartext (H2C)
|
||||
if res.StatusCode == 101 && strings.Contains(strings.ToLower(res.Header.Get("Connection")), "upgrade") && strings.ToLower(res.Header.Get("Upgrade")) == "h2c" {
|
||||
switchingProtocolsHTTP2 = true
|
||||
}
|
||||
|
||||
var body []byte
|
||||
body, err = ioutil.ReadAll(res.Body)
|
||||
res.Body = io.NopCloser(bytes.NewBuffer(body)) // rewind
|
||||
|
||||
ident := fmt.Sprintf(
|
||||
"%s->%s %s->%s %d",
|
||||
"%s->%s %s->%s %d %s",
|
||||
tcpID.DstIP,
|
||||
tcpID.SrcIP,
|
||||
tcpID.DstPort,
|
||||
tcpID.SrcPort,
|
||||
counterPair.Response,
|
||||
"HTTP1",
|
||||
)
|
||||
item := reqResMatcher.registerResponse(ident, res, superTimer.CaptureTime)
|
||||
if item != nil {
|
||||
@@ -145,5 +163,5 @@ func handleHTTP1ServerStream(b *bufio.Reader, tcpID *api.TcpID, counterPair *api
|
||||
}
|
||||
filterAndEmit(item, emitter, options)
|
||||
}
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
@@ -85,7 +86,15 @@ func (d dissecting) Dissect(b *bufio.Reader, isClient bool, tcpID *api.TcpID, co
|
||||
}
|
||||
|
||||
dissected := false
|
||||
switchingProtocolsHTTP2 := false
|
||||
for {
|
||||
if switchingProtocolsHTTP2 {
|
||||
switchingProtocolsHTTP2 = false
|
||||
isHTTP2, err = checkIsHTTP2Connection(b, isClient)
|
||||
prepareHTTP2Connection(b, isClient)
|
||||
http2Assembler = createHTTP2Assembler(b)
|
||||
}
|
||||
|
||||
if superIdentifier.Protocol != nil && superIdentifier.Protocol != &protocol {
|
||||
return errors.New("Identified by another protocol")
|
||||
}
|
||||
@@ -99,15 +108,39 @@ func (d dissecting) Dissect(b *bufio.Reader, isClient bool, tcpID *api.TcpID, co
|
||||
}
|
||||
dissected = true
|
||||
} else if isClient {
|
||||
err = handleHTTP1ClientStream(b, tcpID, counterPair, superTimer, emitter, options)
|
||||
var req *http.Request
|
||||
switchingProtocolsHTTP2, req, err = handleHTTP1ClientStream(b, tcpID, counterPair, superTimer, emitter, options)
|
||||
if err == io.EOF || err == io.ErrUnexpectedEOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
continue
|
||||
}
|
||||
dissected = true
|
||||
|
||||
// In case of an HTTP2 upgrade, duplicate the HTTP1 request into HTTP2 with stream ID 1
|
||||
if switchingProtocolsHTTP2 {
|
||||
ident := fmt.Sprintf(
|
||||
"%s->%s %s->%s 1 %s",
|
||||
tcpID.SrcIP,
|
||||
tcpID.DstIP,
|
||||
tcpID.SrcPort,
|
||||
tcpID.DstPort,
|
||||
"HTTP2",
|
||||
)
|
||||
item := reqResMatcher.registerRequest(ident, req, superTimer.CaptureTime)
|
||||
if item != nil {
|
||||
item.ConnectionInfo = &api.ConnectionInfo{
|
||||
ClientIP: tcpID.SrcIP,
|
||||
ClientPort: tcpID.SrcPort,
|
||||
ServerIP: tcpID.DstIP,
|
||||
ServerPort: tcpID.DstPort,
|
||||
IsOutgoing: true,
|
||||
}
|
||||
filterAndEmit(item, emitter, options)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
err = handleHTTP1ServerStream(b, tcpID, counterPair, superTimer, emitter, options)
|
||||
switchingProtocolsHTTP2, err = handleHTTP1ServerStream(b, tcpID, counterPair, superTimer, emitter, options)
|
||||
if err == io.EOF || err == io.ErrUnexpectedEOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
@@ -125,13 +158,15 @@ func (d dissecting) Dissect(b *bufio.Reader, isClient bool, tcpID *api.TcpID, co
|
||||
}
|
||||
|
||||
func (d dissecting) Analyze(item *api.OutputChannelItem, resolvedSource string, resolvedDestination string) *api.MizuEntry {
|
||||
var host, authority, path, service string
|
||||
var host, authority, path string
|
||||
|
||||
request := item.Pair.Request.Payload.(map[string]interface{})
|
||||
response := item.Pair.Response.Payload.(map[string]interface{})
|
||||
reqDetails := request["details"].(map[string]interface{})
|
||||
resDetails := response["details"].(map[string]interface{})
|
||||
|
||||
isRequestUpgradedH2C := false
|
||||
|
||||
for _, header := range reqDetails["headers"].([]interface{}) {
|
||||
h := header.(map[string]interface{})
|
||||
if h["name"] == "Host" {
|
||||
@@ -143,16 +178,26 @@ func (d dissecting) Analyze(item *api.OutputChannelItem, resolvedSource string,
|
||||
if h["name"] == ":path" {
|
||||
path = h["value"].(string)
|
||||
}
|
||||
|
||||
if h["name"] == "Upgrade" {
|
||||
if h["value"].(string) == "h2c" {
|
||||
isRequestUpgradedH2C = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if resDetails["bodySize"].(float64) < 0 {
|
||||
resDetails["bodySize"] = 0
|
||||
}
|
||||
|
||||
if item.Protocol.Version == "2.0" {
|
||||
service = authority
|
||||
if item.Protocol.Version == "2.0" && !isRequestUpgradedH2C {
|
||||
if resolvedDestination == "" {
|
||||
resolvedDestination = authority
|
||||
}
|
||||
if resolvedDestination == "" {
|
||||
resolvedDestination = host
|
||||
}
|
||||
} else {
|
||||
service = host
|
||||
u, err := url.Parse(reqDetails["url"].(string))
|
||||
if err != nil {
|
||||
path = reqDetails["url"].(string)
|
||||
@@ -162,6 +207,7 @@ func (d dissecting) Analyze(item *api.OutputChannelItem, resolvedSource string,
|
||||
}
|
||||
|
||||
request["url"] = reqDetails["url"].(string)
|
||||
reqDetails["targetUri"] = reqDetails["url"]
|
||||
reqDetails["path"] = path
|
||||
reqDetails["summary"] = path
|
||||
|
||||
@@ -179,19 +225,13 @@ func (d dissecting) Analyze(item *api.OutputChannelItem, resolvedSource string,
|
||||
reqDetails["_queryString"] = reqDetails["queryString"]
|
||||
reqDetails["queryString"] = mapSliceRebuildAsMap(reqDetails["_queryString"].([]interface{}))
|
||||
|
||||
if resolvedDestination != "" {
|
||||
service = resolvedDestination
|
||||
} else if resolvedSource != "" {
|
||||
service = resolvedSource
|
||||
}
|
||||
|
||||
method := reqDetails["method"].(string)
|
||||
statusCode := int(resDetails["status"].(float64))
|
||||
if item.Protocol.Abbreviation == "gRPC" {
|
||||
resDetails["statusText"] = grpcStatusCodes[statusCode]
|
||||
}
|
||||
|
||||
if item.Protocol.Version == "2.0" {
|
||||
if item.Protocol.Version == "2.0" && !isRequestUpgradedH2C {
|
||||
reqDetails["url"] = path
|
||||
request["url"] = path
|
||||
}
|
||||
@@ -213,47 +253,33 @@ func (d dissecting) Analyze(item *api.OutputChannelItem, resolvedSource string,
|
||||
IP: item.ConnectionInfo.ServerIP,
|
||||
Port: item.ConnectionInfo.ServerPort,
|
||||
},
|
||||
Outgoing: item.ConnectionInfo.IsOutgoing,
|
||||
Request: reqDetails,
|
||||
Response: resDetails,
|
||||
Url: fmt.Sprintf("%s%s", service, path),
|
||||
Method: method,
|
||||
Status: statusCode,
|
||||
RequestSenderIp: item.ConnectionInfo.ClientIP,
|
||||
Service: service,
|
||||
Timestamp: item.Timestamp,
|
||||
StartTime: item.Pair.Request.CaptureTime,
|
||||
ElapsedTime: elapsedTime,
|
||||
Summary: path,
|
||||
ResolvedSource: resolvedSource,
|
||||
ResolvedDestination: resolvedDestination,
|
||||
SourceIp: item.ConnectionInfo.ClientIP,
|
||||
DestinationIp: item.ConnectionInfo.ServerIP,
|
||||
SourcePort: item.ConnectionInfo.ClientPort,
|
||||
DestinationPort: item.ConnectionInfo.ServerPort,
|
||||
IsOutgoing: item.ConnectionInfo.IsOutgoing,
|
||||
HTTPPair: string(httpPair),
|
||||
Outgoing: item.ConnectionInfo.IsOutgoing,
|
||||
Request: reqDetails,
|
||||
Response: resDetails,
|
||||
Method: method,
|
||||
Status: statusCode,
|
||||
Timestamp: item.Timestamp,
|
||||
StartTime: item.Pair.Request.CaptureTime,
|
||||
ElapsedTime: elapsedTime,
|
||||
Summary: path,
|
||||
IsOutgoing: item.ConnectionInfo.IsOutgoing,
|
||||
HTTPPair: string(httpPair),
|
||||
}
|
||||
}
|
||||
|
||||
func (d dissecting) Summarize(entry *api.MizuEntry) *api.BaseEntryDetails {
|
||||
return &api.BaseEntryDetails{
|
||||
Id: entry.Id,
|
||||
Protocol: entry.Protocol,
|
||||
Url: entry.Url,
|
||||
RequestSenderIp: entry.RequestSenderIp,
|
||||
Service: entry.Service,
|
||||
Path: entry.Path,
|
||||
Summary: entry.Summary,
|
||||
StatusCode: entry.Status,
|
||||
Method: entry.Method,
|
||||
Timestamp: entry.Timestamp,
|
||||
SourceIp: entry.SourceIp,
|
||||
DestinationIp: entry.DestinationIp,
|
||||
SourcePort: entry.SourcePort,
|
||||
DestinationPort: entry.DestinationPort,
|
||||
IsOutgoing: entry.IsOutgoing,
|
||||
Latency: entry.ElapsedTime,
|
||||
Id: entry.Id,
|
||||
Protocol: entry.Protocol,
|
||||
Path: entry.Path,
|
||||
Summary: entry.Summary,
|
||||
StatusCode: entry.Status,
|
||||
Method: entry.Method,
|
||||
Timestamp: entry.Timestamp,
|
||||
Source: entry.Source,
|
||||
Destination: entry.Destination,
|
||||
IsOutgoing: entry.IsOutgoing,
|
||||
Latency: entry.ElapsedTime,
|
||||
Rules: api.ApplicableRules{
|
||||
Latency: 0,
|
||||
Status: false,
|
||||
@@ -269,9 +295,9 @@ func representRequest(request map[string]interface{}) (repRequest []interface{})
|
||||
Selector: `request.method`,
|
||||
},
|
||||
{
|
||||
Name: "URL",
|
||||
Value: request["url"].(string),
|
||||
Selector: `request.url`,
|
||||
Name: "Target URI",
|
||||
Value: request["targetUri"].(string),
|
||||
Selector: `request.targetUri`,
|
||||
},
|
||||
{
|
||||
Name: "Path",
|
||||
|
||||
@@ -92,6 +92,6 @@ func splitIdent(ident string) []string {
|
||||
}
|
||||
|
||||
func genKey(split []string) string {
|
||||
key := fmt.Sprintf("%s:%s->%s:%s,%s", split[0], split[2], split[1], split[3], split[4])
|
||||
key := fmt.Sprintf("%s:%s->%s:%s,%s%s", split[0], split[2], split[1], split[3], split[4], split[5])
|
||||
return key
|
||||
}
|
||||
|
||||
@@ -65,12 +65,6 @@ func (d dissecting) Dissect(b *bufio.Reader, isClient bool, tcpID *api.TcpID, co
|
||||
func (d dissecting) Analyze(item *api.OutputChannelItem, resolvedSource string, resolvedDestination string) *api.MizuEntry {
|
||||
request := item.Pair.Request.Payload.(map[string]interface{})
|
||||
reqDetails := request["details"].(map[string]interface{})
|
||||
service := "kafka"
|
||||
if resolvedDestination != "" {
|
||||
service = resolvedDestination
|
||||
} else if resolvedSource != "" {
|
||||
service = resolvedSource
|
||||
}
|
||||
apiKey := ApiKey(reqDetails["apiKey"].(float64))
|
||||
|
||||
summary := ""
|
||||
@@ -164,45 +158,31 @@ func (d dissecting) Analyze(item *api.OutputChannelItem, resolvedSource string,
|
||||
IP: item.ConnectionInfo.ServerIP,
|
||||
Port: item.ConnectionInfo.ServerPort,
|
||||
},
|
||||
Outgoing: item.ConnectionInfo.IsOutgoing,
|
||||
Request: reqDetails,
|
||||
Response: item.Pair.Response.Payload.(map[string]interface{})["details"].(map[string]interface{}),
|
||||
Url: fmt.Sprintf("%s%s", service, summary),
|
||||
Method: apiNames[apiKey],
|
||||
Status: 0,
|
||||
RequestSenderIp: item.ConnectionInfo.ClientIP,
|
||||
Service: service,
|
||||
Timestamp: item.Timestamp,
|
||||
StartTime: item.Pair.Request.CaptureTime,
|
||||
ElapsedTime: elapsedTime,
|
||||
Summary: summary,
|
||||
ResolvedSource: resolvedSource,
|
||||
ResolvedDestination: resolvedDestination,
|
||||
SourceIp: item.ConnectionInfo.ClientIP,
|
||||
DestinationIp: item.ConnectionInfo.ServerIP,
|
||||
SourcePort: item.ConnectionInfo.ClientPort,
|
||||
DestinationPort: item.ConnectionInfo.ServerPort,
|
||||
IsOutgoing: item.ConnectionInfo.IsOutgoing,
|
||||
Outgoing: item.ConnectionInfo.IsOutgoing,
|
||||
Request: reqDetails,
|
||||
Response: item.Pair.Response.Payload.(map[string]interface{})["details"].(map[string]interface{}),
|
||||
Method: apiNames[apiKey],
|
||||
Status: 0,
|
||||
Timestamp: item.Timestamp,
|
||||
StartTime: item.Pair.Request.CaptureTime,
|
||||
ElapsedTime: elapsedTime,
|
||||
Summary: summary,
|
||||
IsOutgoing: item.ConnectionInfo.IsOutgoing,
|
||||
}
|
||||
}
|
||||
|
||||
func (d dissecting) Summarize(entry *api.MizuEntry) *api.BaseEntryDetails {
|
||||
return &api.BaseEntryDetails{
|
||||
Id: entry.Id,
|
||||
Protocol: _protocol,
|
||||
Url: entry.Url,
|
||||
RequestSenderIp: entry.RequestSenderIp,
|
||||
Service: entry.Service,
|
||||
Summary: entry.Summary,
|
||||
StatusCode: entry.Status,
|
||||
Method: entry.Method,
|
||||
Timestamp: entry.Timestamp,
|
||||
SourceIp: entry.SourceIp,
|
||||
DestinationIp: entry.DestinationIp,
|
||||
SourcePort: entry.SourcePort,
|
||||
DestinationPort: entry.DestinationPort,
|
||||
IsOutgoing: entry.IsOutgoing,
|
||||
Latency: entry.ElapsedTime,
|
||||
Id: entry.Id,
|
||||
Protocol: _protocol,
|
||||
Summary: entry.Summary,
|
||||
StatusCode: entry.Status,
|
||||
Method: entry.Method,
|
||||
Timestamp: entry.Timestamp,
|
||||
Source: entry.Source,
|
||||
Destination: entry.Destination,
|
||||
IsOutgoing: entry.IsOutgoing,
|
||||
Latency: entry.ElapsedTime,
|
||||
Rules: api.ApplicableRules{
|
||||
Latency: 0,
|
||||
Status: false,
|
||||
|
||||
@@ -65,13 +65,6 @@ func (d dissecting) Analyze(item *api.OutputChannelItem, resolvedSource string,
|
||||
reqDetails := request["details"].(map[string]interface{})
|
||||
resDetails := response["details"].(map[string]interface{})
|
||||
|
||||
service := "redis"
|
||||
if resolvedDestination != "" {
|
||||
service = resolvedDestination
|
||||
} else if resolvedSource != "" {
|
||||
service = resolvedSource
|
||||
}
|
||||
|
||||
method := ""
|
||||
if reqDetails["command"] != nil {
|
||||
method = reqDetails["command"].(string)
|
||||
@@ -99,46 +92,32 @@ func (d dissecting) Analyze(item *api.OutputChannelItem, resolvedSource string,
|
||||
IP: item.ConnectionInfo.ServerIP,
|
||||
Port: item.ConnectionInfo.ServerPort,
|
||||
},
|
||||
Outgoing: item.ConnectionInfo.IsOutgoing,
|
||||
Request: reqDetails,
|
||||
Response: resDetails,
|
||||
Url: fmt.Sprintf("%s%s", service, summary),
|
||||
Method: method,
|
||||
Status: 0,
|
||||
RequestSenderIp: item.ConnectionInfo.ClientIP,
|
||||
Service: service,
|
||||
Timestamp: item.Timestamp,
|
||||
StartTime: item.Pair.Request.CaptureTime,
|
||||
ElapsedTime: elapsedTime,
|
||||
Summary: summary,
|
||||
ResolvedSource: resolvedSource,
|
||||
ResolvedDestination: resolvedDestination,
|
||||
SourceIp: item.ConnectionInfo.ClientIP,
|
||||
DestinationIp: item.ConnectionInfo.ServerIP,
|
||||
SourcePort: item.ConnectionInfo.ClientPort,
|
||||
DestinationPort: item.ConnectionInfo.ServerPort,
|
||||
IsOutgoing: item.ConnectionInfo.IsOutgoing,
|
||||
Outgoing: item.ConnectionInfo.IsOutgoing,
|
||||
Request: reqDetails,
|
||||
Response: resDetails,
|
||||
Method: method,
|
||||
Status: 0,
|
||||
Timestamp: item.Timestamp,
|
||||
StartTime: item.Pair.Request.CaptureTime,
|
||||
ElapsedTime: elapsedTime,
|
||||
Summary: summary,
|
||||
IsOutgoing: item.ConnectionInfo.IsOutgoing,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (d dissecting) Summarize(entry *api.MizuEntry) *api.BaseEntryDetails {
|
||||
return &api.BaseEntryDetails{
|
||||
Id: entry.Id,
|
||||
Protocol: protocol,
|
||||
Url: entry.Url,
|
||||
RequestSenderIp: entry.RequestSenderIp,
|
||||
Service: entry.Service,
|
||||
Summary: entry.Summary,
|
||||
StatusCode: entry.Status,
|
||||
Method: entry.Method,
|
||||
Timestamp: entry.Timestamp,
|
||||
SourceIp: entry.SourceIp,
|
||||
DestinationIp: entry.DestinationIp,
|
||||
SourcePort: entry.SourcePort,
|
||||
DestinationPort: entry.DestinationPort,
|
||||
IsOutgoing: entry.IsOutgoing,
|
||||
Latency: entry.ElapsedTime,
|
||||
Id: entry.Id,
|
||||
Protocol: protocol,
|
||||
Summary: entry.Summary,
|
||||
StatusCode: entry.Status,
|
||||
Method: entry.Method,
|
||||
Timestamp: entry.Timestamp,
|
||||
Source: entry.Source,
|
||||
Destination: entry.Destination,
|
||||
IsOutgoing: entry.IsOutgoing,
|
||||
Latency: entry.ElapsedTime,
|
||||
Rules: api.ApplicableRules{
|
||||
Latency: 0,
|
||||
Status: false,
|
||||
|
||||
11
ui/package-lock.json
generated
11
ui/package-lock.json
generated
@@ -11080,6 +11080,11 @@
|
||||
"minimist": "^1.2.5"
|
||||
}
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.29.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
|
||||
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
|
||||
},
|
||||
"move-concurrently": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
|
||||
@@ -13644,9 +13649,9 @@
|
||||
}
|
||||
},
|
||||
"react-scrollable-feed-virtualized": {
|
||||
"version": "1.4.8",
|
||||
"resolved": "https://registry.npmjs.org/react-scrollable-feed-virtualized/-/react-scrollable-feed-virtualized-1.4.8.tgz",
|
||||
"integrity": "sha512-zsSO/9QB+4V6HEk39lxeMEUA6JFSZjfV4stw7RF17+vZdlVhyATsTBCzsj8hZywY4F29cBfH+3/GKrMhwmhAsw=="
|
||||
"version": "1.4.9",
|
||||
"resolved": "https://registry.npmjs.org/react-scrollable-feed-virtualized/-/react-scrollable-feed-virtualized-1.4.9.tgz",
|
||||
"integrity": "sha512-YkFkPjdIXDUsaCNYhZ+Blpp3LF+CsJWscwn/0fGSjF5QBKCtPURO9AEUA362Qnjr4S8LF2IjSAOCCFedIEnVNw=="
|
||||
},
|
||||
"react-syntax-highlighter": {
|
||||
"version": "15.4.3",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"@uiw/react-textarea-code-editor": "^1.4.12",
|
||||
"axios": "^0.21.1",
|
||||
"jsonpath": "^1.1.1",
|
||||
"moment": "^2.29.1",
|
||||
"node-sass": "^5.0.0",
|
||||
"numeral": "^2.0.6",
|
||||
"protobuf-decoder": "^0.1.0",
|
||||
@@ -23,7 +24,7 @@
|
||||
"react-copy-to-clipboard": "^5.0.3",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-scripts": "4.0.3",
|
||||
"react-scrollable-feed-virtualized": "^1.4.8",
|
||||
"react-scrollable-feed-virtualized": "^1.4.9",
|
||||
"react-syntax-highlighter": "^15.4.3",
|
||||
"react-toastify": "^8.0.3",
|
||||
"typescript": "^4.2.4",
|
||||
|
||||
@@ -1,33 +1,140 @@
|
||||
import React, {useRef} from "react";
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from "react";
|
||||
import styles from './style/EntriesList.module.sass';
|
||||
import ScrollableFeedVirtualized from "react-scrollable-feed-virtualized";
|
||||
import Moment from 'moment';
|
||||
import {EntryItem} from "./EntryListItem/EntryListItem";
|
||||
import down from "./assets/downImg.svg";
|
||||
import spinner from './assets/spinner.svg';
|
||||
import Api from "../helpers/api";
|
||||
|
||||
interface EntriesListProps {
|
||||
entries: any[];
|
||||
setEntries: any;
|
||||
query: string;
|
||||
listEntryREF: any;
|
||||
onSnapBrokenEvent: () => void;
|
||||
isSnappedToBottom: boolean;
|
||||
setIsSnappedToBottom: any;
|
||||
queriedCurrent: number;
|
||||
setQueriedCurrent: any;
|
||||
queriedTotal: number;
|
||||
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;
|
||||
}
|
||||
|
||||
export const EntriesList: React.FC<EntriesListProps> = ({entries, listEntryREF, onSnapBrokenEvent, isSnappedToBottom, setIsSnappedToBottom, queriedCurrent, queriedTotal, startTime}) => {
|
||||
const api = new Api();
|
||||
|
||||
export const EntriesList: React.FC<EntriesListProps> = ({entries, setEntries, query, listEntryREF, onSnapBrokenEvent, isSnappedToBottom, setIsSnappedToBottom, queriedCurrent, setQueriedCurrent, queriedTotal, startTime, noMoreDataTop, setNoMoreDataTop, focusedEntryId, setFocusedEntryId, updateQuery, leftOffTop, setLeftOffTop, isWebSocketConnectionClosed, ws, openWebSocket, leftOffBottom}) => {
|
||||
const [loadMoreTop, setLoadMoreTop] = useState(false);
|
||||
const [isLoadingTop, setIsLoadingTop] = useState(false);
|
||||
const scrollableRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const list = document.getElementById('list').firstElementChild;
|
||||
list.addEventListener('scroll', (e) => {
|
||||
const el: any = e.target;
|
||||
if(el.scrollTop === 0) {
|
||||
setLoadMoreTop(true);
|
||||
} else {
|
||||
setNoMoreDataTop(false);
|
||||
setLoadMoreTop(false);
|
||||
}
|
||||
});
|
||||
}, [setLoadMoreTop, setNoMoreDataTop]);
|
||||
|
||||
const memoizedEntries = useMemo(() => {
|
||||
return entries;
|
||||
},[entries]);
|
||||
|
||||
const getOldEntries = useCallback(async () => {
|
||||
setLoadMoreTop(false);
|
||||
if (leftOffTop === null || leftOffTop <= 0) {
|
||||
return;
|
||||
}
|
||||
setIsLoadingTop(true);
|
||||
const data = await api.fetchEntries(leftOffTop, -1, query, 100, 3000);
|
||||
if (!data || !data.meta) {
|
||||
setNoMoreDataTop(true);
|
||||
setIsLoadingTop(false);
|
||||
return;
|
||||
}
|
||||
setLeftOffTop(data.meta.leftOff);
|
||||
|
||||
let scrollTo: boolean;
|
||||
if (data.meta.leftOff === 0) {
|
||||
setNoMoreDataTop(true);
|
||||
scrollTo = false;
|
||||
} else {
|
||||
scrollTo = true;
|
||||
}
|
||||
setIsLoadingTop(false);
|
||||
|
||||
const newEntries = [...data.data.reverse(), ...entries];
|
||||
setEntries(newEntries);
|
||||
|
||||
setQueriedCurrent(queriedCurrent + data.meta.current);
|
||||
|
||||
if (scrollTo) {
|
||||
scrollableRef.current.scrollToIndex(data.data.length - 1);
|
||||
}
|
||||
},[setLoadMoreTop, setIsLoadingTop, entries, setEntries, query, setNoMoreDataTop, leftOffTop, setLeftOffTop, queriedCurrent, setQueriedCurrent]);
|
||||
|
||||
useEffect(() => {
|
||||
if(!isWebSocketConnectionClosed || !loadMoreTop || noMoreDataTop) return;
|
||||
getOldEntries();
|
||||
}, [loadMoreTop, noMoreDataTop, getOldEntries, isWebSocketConnectionClosed]);
|
||||
|
||||
const scrollbarVisible = scrollableRef.current?.childWrapperRef.current.clientHeight > scrollableRef.current?.wrapperRef.current.clientHeight;
|
||||
|
||||
return <>
|
||||
<div className={styles.list}>
|
||||
<div id="list" ref={listEntryREF} className={styles.list}>
|
||||
{isLoadingTop && <div className={styles.spinnerContainer}>
|
||||
<img alt="spinner" src={spinner} style={{height: 25}}/>
|
||||
</div>}
|
||||
{noMoreDataTop && <div id="noMoreDataTop" className={styles.noMoreDataAvailable}>No more data available</div>}
|
||||
<ScrollableFeedVirtualized ref={scrollableRef} itemHeight={48} marginTop={10} onSnapBroken={onSnapBrokenEvent}>
|
||||
{false /* TODO: why there is a need for something here (not necessarily false)? */}
|
||||
{entries}
|
||||
{false /* It's because the first child is ignored by ScrollableFeedVirtualized */}
|
||||
{memoizedEntries.map(entry => <EntryItem
|
||||
key={`entry-${entry.id}`}
|
||||
entry={entry}
|
||||
focusedEntryId={focusedEntryId}
|
||||
setFocusedEntryId={setFocusedEntryId}
|
||||
style={{}}
|
||||
updateQuery={updateQuery}
|
||||
headingMode={false}
|
||||
/>)}
|
||||
</ScrollableFeedVirtualized>
|
||||
<button type="button"
|
||||
className={`${styles.btnLive} ${isSnappedToBottom ? styles.hideButton : styles.showButton}`}
|
||||
title="Fetch old records"
|
||||
className={`${styles.btnOld} ${!scrollbarVisible && leftOffTop > 0 ? styles.showButton : styles.hideButton}`}
|
||||
onClick={(_) => {
|
||||
ws.close();
|
||||
getOldEntries();
|
||||
}}>
|
||||
<img alt="down" src={down} />
|
||||
</button>
|
||||
<button type="button"
|
||||
title="Snap to bottom"
|
||||
className={`${styles.btnLive} ${isSnappedToBottom && !isWebSocketConnectionClosed ? styles.hideButton : styles.showButton}`}
|
||||
onClick={(_) => {
|
||||
if (isWebSocketConnectionClosed) {
|
||||
if (query) {
|
||||
openWebSocket(`(${query}) and leftOff(${leftOffBottom})`, false);
|
||||
} else {
|
||||
openWebSocket(`leftOff(${leftOffBottom})`, false);
|
||||
}
|
||||
}
|
||||
scrollableRef.current.jumpToBottom();
|
||||
setIsSnappedToBottom(true);
|
||||
}}>
|
||||
@@ -36,8 +143,8 @@ export const EntriesList: React.FC<EntriesListProps> = ({entries, listEntryREF,
|
||||
</div>
|
||||
|
||||
<div className={styles.footer}>
|
||||
<div>Displaying <b>{entries?.length}</b> results (queried <b>{queriedCurrent}</b>/<b>{queriedTotal}</b>)</div>
|
||||
{startTime !== 0 && <div>Started listening at <span style={{marginRight: 5, fontWeight: 600, fontSize: 13}}>{new Date(startTime).toLocaleString()}</span></div>}
|
||||
<div>Displaying <b>{entries?.length}</b> results out of <b>{queriedTotal}</b> total</div>
|
||||
{startTime !== 0 && <div>Started listening at <span style={{marginRight: 5, fontWeight: 600, fontSize: 13}}>{Moment(startTime).utc().format('MM/DD/YYYY, h:mm:ss.SSS A')}</span></div>}
|
||||
</div>
|
||||
</div>
|
||||
</>;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from "react";
|
||||
import EntryViewer from "./EntryDetailed/EntryViewer";
|
||||
import {EntryItem} from "./EntryListItem/EntryListItem";
|
||||
import {makeStyles} from "@material-ui/core";
|
||||
import Protocol from "./UI/Protocol"
|
||||
import StatusCode from "./UI/StatusCode";
|
||||
import {Summary} from "./UI/Summary";
|
||||
import Queryable from "./UI/Queryable";
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
entryTitle: {
|
||||
@@ -12,6 +12,7 @@ const useStyles = makeStyles(() => ({
|
||||
maxHeight: 46,
|
||||
alignItems: 'center',
|
||||
marginBottom: 4,
|
||||
marginLeft: 6,
|
||||
padding: 2,
|
||||
paddingBottom: 0
|
||||
},
|
||||
@@ -37,45 +38,49 @@ const EntryTitle: React.FC<any> = ({protocol, data, bodySize, elapsedTime, updat
|
||||
const classes = useStyles();
|
||||
const response = data.response;
|
||||
|
||||
|
||||
return <div className={classes.entryTitle}>
|
||||
<Protocol protocol={protocol} horizontal={true} updateQuery={null}/>
|
||||
<Protocol protocol={protocol} horizontal={true} updateQuery={updateQuery}/>
|
||||
<div style={{right: "30px", position: "absolute", display: "flex"}}>
|
||||
{response && <div
|
||||
className="queryable"
|
||||
style={{margin: "0 18px", opacity: 0.5}}
|
||||
onClick={() => {
|
||||
updateQuery(`response.bodySize == ${bodySize}`)
|
||||
}}
|
||||
{response && <Queryable
|
||||
query={`response.bodySize == ${bodySize}`}
|
||||
updateQuery={updateQuery}
|
||||
style={{margin: "0 18px"}}
|
||||
displayIconOnMouseOver={true}
|
||||
>
|
||||
{formatSize(bodySize)}
|
||||
</div>}
|
||||
{response && <div
|
||||
className="queryable"
|
||||
style={{marginRight: 18, opacity: 0.5}}
|
||||
onClick={() => {
|
||||
updateQuery(`elapsedTime >= ${elapsedTime}`)
|
||||
}}
|
||||
<div
|
||||
style={{opacity: 0.5}}
|
||||
>
|
||||
{formatSize(bodySize)}
|
||||
</div>
|
||||
</Queryable>}
|
||||
{response && <Queryable
|
||||
query={`elapsedTime >= ${elapsedTime}`}
|
||||
updateQuery={updateQuery}
|
||||
style={{marginRight: 18}}
|
||||
displayIconOnMouseOver={true}
|
||||
>
|
||||
{Math.round(elapsedTime)}ms
|
||||
</div>}
|
||||
<div
|
||||
style={{opacity: 0.5}}
|
||||
>
|
||||
{Math.round(elapsedTime)}ms
|
||||
</div>
|
||||
</Queryable>}
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
const EntrySummary: React.FC<any> = ({data, updateQuery}) => {
|
||||
const classes = useStyles();
|
||||
const entry = data.base;
|
||||
|
||||
const response = data.response;
|
||||
|
||||
return <div className={classes.entrySummary}>
|
||||
{response && "status" in response && <div style={{marginRight: 8}}>
|
||||
<StatusCode statusCode={response.status} updateQuery={updateQuery}/>
|
||||
</div>}
|
||||
<div style={{flexGrow: 1, overflow: 'hidden'}}>
|
||||
<Summary method={data.method} summary={data.summary} updateQuery={updateQuery}/>
|
||||
</div>
|
||||
</div>;
|
||||
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}) => {
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
font-weight: 600
|
||||
font-size: .75rem
|
||||
line-height: 1.2
|
||||
margin: .3rem 0
|
||||
margin-bottom: -2px
|
||||
|
||||
.dataKey
|
||||
color: $blue-gray
|
||||
|
||||
@@ -3,6 +3,7 @@ import React, {useState} from "react";
|
||||
import {SyntaxHighlighter} from "../UI/SyntaxHighlighter/index";
|
||||
import CollapsibleContainer from "../UI/CollapsibleContainer";
|
||||
import FancyTextDisplay from "../UI/FancyTextDisplay";
|
||||
import Queryable from "../UI/Queryable";
|
||||
import Checkbox from "../UI/Checkbox";
|
||||
import ProtobufDecoder from "protobuf-decoder";
|
||||
|
||||
@@ -15,23 +16,29 @@ interface EntryViewLineProps {
|
||||
}
|
||||
|
||||
const EntryViewLine: React.FC<EntryViewLineProps> = ({label, value, updateQuery, selector, overrideQueryValue}) => {
|
||||
let query: string;
|
||||
if (!selector) {
|
||||
query = "";
|
||||
} else if (overrideQueryValue) {
|
||||
query = `${selector} == ${overrideQueryValue}`;
|
||||
} else if (typeof(value) == "string") {
|
||||
query = `${selector} == "${JSON.stringify(value).slice(1, -1)}"`;
|
||||
} else {
|
||||
query = `${selector} == ${value}`;
|
||||
}
|
||||
return (label && <tr className={styles.dataLine}>
|
||||
<td
|
||||
className={`queryable ${styles.dataKey}`}
|
||||
onClick={() => {
|
||||
if (!selector) {
|
||||
return
|
||||
} else if (overrideQueryValue) {
|
||||
updateQuery(`${selector} == ${overrideQueryValue}`)
|
||||
} else if (typeof(value) === "string") {
|
||||
updateQuery(`${selector} == "${JSON.stringify(value).slice(1, -1)}"`)
|
||||
} else {
|
||||
updateQuery(`${selector} == ${value}`)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</td>
|
||||
<td className={`${styles.dataKey}`}>
|
||||
<Queryable
|
||||
query={query}
|
||||
updateQuery={updateQuery}
|
||||
style={{float: "right", height: "18px"}}
|
||||
iconStyle={{marginRight: "20px"}}
|
||||
flipped={true}
|
||||
displayIconOnMouseOver={true}
|
||||
>
|
||||
{label}
|
||||
</Queryable>
|
||||
</td>
|
||||
<td>
|
||||
<FancyTextDisplay
|
||||
className={styles.dataValue}
|
||||
@@ -53,9 +60,9 @@ interface EntrySectionCollapsibleTitleProps {
|
||||
|
||||
const EntrySectionCollapsibleTitle: React.FC<EntrySectionCollapsibleTitleProps> = ({title, color, isExpanded}) => {
|
||||
return <div className={styles.title}>
|
||||
<span className={`${styles.button} ${isExpanded ? styles.expanded : ''}`} style={{backgroundColor: color}}>
|
||||
<div className={`${styles.button} ${isExpanded ? styles.expanded : ''}`} style={{backgroundColor: color}}>
|
||||
{isExpanded ? '-' : '+'}
|
||||
</span>
|
||||
</div>
|
||||
<span>{title}</span>
|
||||
</div>
|
||||
}
|
||||
@@ -130,7 +137,7 @@ export const EntryBodySection: React.FC<EntryBodySectionProps> = ({
|
||||
<table>
|
||||
<tbody>
|
||||
<EntryViewLine label={'Mime type'} value={contentType} updateQuery={updateQuery} selector={selector} overrideQueryValue={`r".*"`}/>
|
||||
<EntryViewLine label={'Encoding'} value={encoding} updateQuery={updateQuery} selector={selector} overrideQueryValue={`r".*"`}/>
|
||||
{encoding && <EntryViewLine label={'Encoding'} value={encoding} updateQuery={updateQuery} selector={selector} overrideQueryValue={`r".*"`}/>}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
font-family: "Source Sans Pro", Lucida Grande, Tahoma, sans-serif
|
||||
height: calc(100% - 70px)
|
||||
width: 100%
|
||||
margin-top: 10px
|
||||
|
||||
h3,
|
||||
h4
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
.rowSelected
|
||||
border: 1px $blue-color solid
|
||||
margin-right: 3px
|
||||
|
||||
.ruleSuccessRow
|
||||
background: #E8FFF1
|
||||
@@ -46,13 +45,12 @@
|
||||
.ruleNumberTextSuccess
|
||||
color: #219653
|
||||
|
||||
.service
|
||||
.resolvedName
|
||||
text-overflow: ellipsis
|
||||
overflow: hidden
|
||||
white-space: nowrap
|
||||
color: $secondary-font-color
|
||||
padding-left: 4px
|
||||
padding-top: 3px
|
||||
padding-right: 10px
|
||||
display: flex
|
||||
font-size: 12px
|
||||
@@ -62,7 +60,7 @@
|
||||
color: $secondary-font-color
|
||||
padding-left: 12px
|
||||
flex-shrink: 0
|
||||
width: 145px
|
||||
width: 185px
|
||||
text-align: left
|
||||
|
||||
.endpointServiceContainer
|
||||
@@ -70,7 +68,6 @@
|
||||
flex-direction: column
|
||||
overflow: hidden
|
||||
padding-right: 10px
|
||||
padding-left: 10px
|
||||
flex-grow: 1
|
||||
|
||||
.separatorRight
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import React, {useState} from "react";
|
||||
import React from "react";
|
||||
import Moment from 'moment';
|
||||
import SwapHorizIcon from '@material-ui/icons/SwapHoriz';
|
||||
import styles from './EntryListItem.module.sass';
|
||||
import StatusCode, {getClassification, StatusCodeClassification} from "../UI/StatusCode";
|
||||
import Protocol, {ProtocolInterface} from "../UI/Protocol"
|
||||
import {Summary} from "../UI/Summary";
|
||||
import Queryable from "../UI/Queryable";
|
||||
import ingoingIconSuccess from "../assets/ingoing-traffic-success.svg"
|
||||
import ingoingIconFailure from "../assets/ingoing-traffic-failure.svg"
|
||||
import ingoingIconNeutral from "../assets/ingoing-traffic-neutral.svg"
|
||||
@@ -10,19 +13,21 @@ import outgoingIconSuccess from "../assets/outgoing-traffic-success.svg"
|
||||
import outgoingIconFailure from "../assets/outgoing-traffic-failure.svg"
|
||||
import outgoingIconNeutral from "../assets/outgoing-traffic-neutral.svg"
|
||||
|
||||
interface TCPInterface {
|
||||
ip: string
|
||||
port: string
|
||||
name: string
|
||||
}
|
||||
|
||||
interface Entry {
|
||||
protocol: ProtocolInterface,
|
||||
method?: string,
|
||||
summary: string,
|
||||
service: string,
|
||||
id: number,
|
||||
statusCode?: number;
|
||||
url?: string;
|
||||
timestamp: Date;
|
||||
sourceIp: string,
|
||||
sourcePort: string,
|
||||
destinationIp: string,
|
||||
destinationPort: string,
|
||||
src: TCPInterface,
|
||||
dst: TCPInterface,
|
||||
isOutgoing?: boolean;
|
||||
latency: number;
|
||||
rules: Rules;
|
||||
@@ -37,14 +42,16 @@ 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, setFocusedEntryId, style, updateQuery}) => {
|
||||
export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocusedEntryId, style, updateQuery, headingMode}) => {
|
||||
|
||||
const [isSelected, setIsSelected] = useState(false);
|
||||
const isSelected = focusedEntryId === entry.id.toString();
|
||||
|
||||
const classification = getClassification(entry.statusCode)
|
||||
const numberOfRules = entry.rules.numberOfRules
|
||||
@@ -116,43 +123,66 @@ export const EntryItem: React.FC<EntryProps> = ({entry, setFocusedEntryId, style
|
||||
break;
|
||||
}
|
||||
|
||||
const isStatusCodeEnabled = ((entry.protocol.name === "http" && "statusCode" in entry) || entry.statusCode !== 0);
|
||||
var endpointServiceContainer = "10px";
|
||||
if (!isStatusCodeEnabled) endpointServiceContainer = "20px";
|
||||
|
||||
return <>
|
||||
<div
|
||||
id={entry.id.toString()}
|
||||
id={`entry-${entry.id.toString()}`}
|
||||
className={`${styles.row}
|
||||
${isSelected && !rule && !contractEnabled ? styles.rowSelected : additionalRulesProperties}`}
|
||||
onClick={() => {
|
||||
setIsSelected(!isSelected);
|
||||
if (!setFocusedEntryId) return;
|
||||
setFocusedEntryId(entry.id.toString());
|
||||
}}
|
||||
style={{
|
||||
border: isSelected ? `1px ${entry.protocol.backgroundColor} solid` : "1px transparent solid",
|
||||
position: "absolute",
|
||||
position: !headingMode ? "absolute" : "unset",
|
||||
top: style['top'],
|
||||
marginTop: style['marginTop'],
|
||||
width: "calc(100% - 25px)",
|
||||
marginTop: !headingMode ? style['marginTop'] : "10px",
|
||||
width: !headingMode ? "calc(100% - 25px)" : "calc(100% - 18px)",
|
||||
}}
|
||||
>
|
||||
<Protocol
|
||||
{!headingMode ? <Protocol
|
||||
protocol={entry.protocol}
|
||||
horizontal={false}
|
||||
updateQuery={updateQuery}
|
||||
/>
|
||||
{((entry.protocol.name === "http" && "statusCode" in entry) || entry.statusCode !== 0) && <div>
|
||||
/> : null}
|
||||
{isStatusCodeEnabled && <div>
|
||||
<StatusCode statusCode={entry.statusCode} updateQuery={updateQuery}/>
|
||||
</div>}
|
||||
<div className={styles.endpointServiceContainer}>
|
||||
<div className={styles.endpointServiceContainer} style={{paddingLeft: endpointServiceContainer}}>
|
||||
<Summary method={entry.method} summary={entry.summary} updateQuery={updateQuery}/>
|
||||
<div className={styles.service}>
|
||||
<span
|
||||
title="Service Name"
|
||||
className="queryable"
|
||||
onClick={() => {
|
||||
updateQuery(`service == "${entry.service}"`)
|
||||
}}
|
||||
<div className={styles.resolvedName}>
|
||||
<Queryable
|
||||
query={`src.name == "${entry.src.name}"`}
|
||||
updateQuery={updateQuery}
|
||||
displayIconOnMouseOver={true}
|
||||
flipped={true}
|
||||
style={{marginTop: "-4px", overflow: "visible"}}
|
||||
iconStyle={!headingMode ? {marginTop: "4px", left: "68px", position: "absolute"} : {marginTop: "4px", left: "calc(50vw + 41px)", position: "absolute"}}
|
||||
>
|
||||
{entry.service}
|
||||
</span>
|
||||
<span
|
||||
title="Source Name"
|
||||
>
|
||||
{entry.src.name ? entry.src.name : "[Unresolved]"}
|
||||
</span>
|
||||
</Queryable>
|
||||
<SwapHorizIcon style={{color: entry.protocol.backgroundColor, marginTop: "-2px"}}></SwapHorizIcon>
|
||||
<Queryable
|
||||
query={`dst.name == "${entry.dst.name}"`}
|
||||
updateQuery={updateQuery}
|
||||
displayIconOnMouseOver={true}
|
||||
style={{marginTop: "-4px"}}
|
||||
iconStyle={{marginTop: "4px", marginLeft: "-2px"}}
|
||||
>
|
||||
<span
|
||||
title="Destination Name"
|
||||
>
|
||||
{entry.dst.name ? entry.dst.name : "[Unresolved]"}
|
||||
</span>
|
||||
</Queryable>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
@@ -170,74 +200,109 @@ export const EntryItem: React.FC<EntryProps> = ({entry, setFocusedEntryId, style
|
||||
: ""
|
||||
}
|
||||
<div className={styles.separatorRight}>
|
||||
<span
|
||||
className={`queryable ${styles.tcpInfo} ${styles.ip}`}
|
||||
title="Source IP"
|
||||
onClick={() => {
|
||||
updateQuery(`src.ip == "${entry.sourceIp}"`)
|
||||
}}
|
||||
<Queryable
|
||||
query={`src.ip == "${entry.src.ip}"`}
|
||||
updateQuery={updateQuery}
|
||||
displayIconOnMouseOver={true}
|
||||
flipped={true}
|
||||
iconStyle={{marginRight: "16px"}}
|
||||
>
|
||||
{entry.sourceIp}
|
||||
</span>
|
||||
<span className={`${styles.tcpInfo}`}>:</span>
|
||||
<span
|
||||
className={`queryable ${styles.tcpInfo} ${styles.port}`}
|
||||
title="Source Port"
|
||||
onClick={() => {
|
||||
updateQuery(`src.port == "${entry.sourcePort}"`)
|
||||
}}
|
||||
<span
|
||||
className={`${styles.tcpInfo} ${styles.ip}`}
|
||||
title="Source IP"
|
||||
>
|
||||
{entry.src.ip}
|
||||
</span>
|
||||
</Queryable>
|
||||
<span className={`${styles.tcpInfo}`} style={{marginTop: "18px"}}>:</span>
|
||||
<Queryable
|
||||
query={`src.port == "${entry.src.port}"`}
|
||||
updateQuery={updateQuery}
|
||||
displayIconOnMouseOver={true}
|
||||
flipped={true}
|
||||
iconStyle={{marginTop: "28px"}}
|
||||
>
|
||||
{entry.sourcePort}
|
||||
</span>
|
||||
<span
|
||||
className={`${styles.tcpInfo} ${styles.port}`}
|
||||
title="Source Port"
|
||||
>
|
||||
{entry.src.port}
|
||||
</span>
|
||||
</Queryable>
|
||||
{entry.isOutgoing ?
|
||||
<img
|
||||
src={outgoingIcon}
|
||||
alt="Ingoing traffic"
|
||||
title="Ingoing"
|
||||
onClick={() => {
|
||||
updateQuery(`outgoing == true`)
|
||||
}}
|
||||
/>
|
||||
<Queryable
|
||||
query={`outgoing == true`}
|
||||
updateQuery={updateQuery}
|
||||
displayIconOnMouseOver={true}
|
||||
flipped={true}
|
||||
iconStyle={{marginTop: "28px"}}
|
||||
>
|
||||
<img
|
||||
src={outgoingIcon}
|
||||
alt="Ingoing traffic"
|
||||
title="Ingoing"
|
||||
/>
|
||||
</Queryable>
|
||||
:
|
||||
<img
|
||||
src={ingoingIcon}
|
||||
alt="Outgoing traffic"
|
||||
title="Outgoing"
|
||||
onClick={() => {
|
||||
updateQuery(`outgoing == false`)
|
||||
}}
|
||||
/>
|
||||
<Queryable
|
||||
query={`outgoing == true`}
|
||||
updateQuery={updateQuery}
|
||||
displayIconOnMouseOver={true}
|
||||
flipped={true}
|
||||
iconStyle={{marginTop: "28px"}}
|
||||
>
|
||||
<img
|
||||
src={ingoingIcon}
|
||||
alt="Outgoing traffic"
|
||||
title="Outgoing"
|
||||
onClick={() => {
|
||||
updateQuery(`outgoing == false`)
|
||||
}}
|
||||
/>
|
||||
</Queryable>
|
||||
}
|
||||
<span
|
||||
className={`queryable ${styles.tcpInfo} ${styles.ip}`}
|
||||
title="Destination IP"
|
||||
onClick={() => {
|
||||
updateQuery(`dst.ip == "${entry.destinationIp}"`)
|
||||
}}
|
||||
<Queryable
|
||||
query={`dst.ip == "${entry.dst.ip}"`}
|
||||
updateQuery={updateQuery}
|
||||
displayIconOnMouseOver={true}
|
||||
flipped={false}
|
||||
iconStyle={{marginTop: "28px"}}
|
||||
>
|
||||
{entry.destinationIp}
|
||||
</span>
|
||||
<span className={`${styles.tcpInfo}`}>:</span>
|
||||
<span
|
||||
className={`queryable ${styles.tcpInfo} ${styles.port}`}
|
||||
title="Destination Port"
|
||||
onClick={() => {
|
||||
updateQuery(`dst.port == "${entry.destinationPort}"`)
|
||||
}}
|
||||
<span
|
||||
className={`${styles.tcpInfo} ${styles.ip}`}
|
||||
title="Destination IP"
|
||||
>
|
||||
{entry.dst.ip}
|
||||
</span>
|
||||
</Queryable>
|
||||
<span className={`${styles.tcpInfo}`} style={{marginTop: "18px"}}>:</span>
|
||||
<Queryable
|
||||
query={`dst.port == "${entry.dst.port}"`}
|
||||
updateQuery={updateQuery}
|
||||
displayIconOnMouseOver={true}
|
||||
flipped={false}
|
||||
>
|
||||
{entry.destinationPort}
|
||||
</span>
|
||||
<span
|
||||
className={`${styles.tcpInfo} ${styles.port}`}
|
||||
title="Destination Port"
|
||||
>
|
||||
{entry.dst.port}
|
||||
</span>
|
||||
</Queryable>
|
||||
</div>
|
||||
<div className={styles.timestamp}>
|
||||
<span
|
||||
title="Timestamp"
|
||||
className="queryable"
|
||||
onClick={() => {
|
||||
updateQuery(`timestamp >= datetime("${new Date(+entry.timestamp)?.toLocaleString("en-US", {timeZone: 'UTC' })}")`)
|
||||
}}
|
||||
<Queryable
|
||||
query={`timestamp >= datetime("${Moment(+entry.timestamp)?.utc().format('MM/DD/YYYY, h:mm:ss.SSS A')}")`}
|
||||
updateQuery={updateQuery}
|
||||
displayIconOnMouseOver={true}
|
||||
flipped={false}
|
||||
>
|
||||
{new Date(+entry.timestamp)?.toLocaleString("en-US")}
|
||||
</span>
|
||||
<span
|
||||
title="Timestamp"
|
||||
>
|
||||
{Moment(+entry.timestamp)?.utc().format('MM/DD/YYYY, h:mm:ss.SSS A')}
|
||||
</span>
|
||||
</Queryable>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -13,7 +13,7 @@ interface FiltersProps {
|
||||
setQuery: any
|
||||
backgroundColor: string
|
||||
ws: any
|
||||
openWebSocket: (query: string) => void;
|
||||
openWebSocket: (query: string, resetEntries: boolean) => void;
|
||||
}
|
||||
|
||||
export const Filters: React.FC<FiltersProps> = ({query, setQuery, backgroundColor, ws, openWebSocket}) => {
|
||||
@@ -33,7 +33,7 @@ interface QueryFormProps {
|
||||
setQuery: any
|
||||
backgroundColor: string
|
||||
ws: any
|
||||
openWebSocket: (query: string) => void;
|
||||
openWebSocket: (query: string, resetEntries: boolean) => void;
|
||||
}
|
||||
|
||||
const style = {
|
||||
@@ -63,8 +63,12 @@ export const QueryForm: React.FC<QueryFormProps> = ({query, setQuery, background
|
||||
}
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
ws.close()
|
||||
openWebSocket(query)
|
||||
ws.close();
|
||||
if (query) {
|
||||
openWebSocket(`(${query}) and leftOff(-1)`, true);
|
||||
} else {
|
||||
openWebSocket(`leftOff(-1)`, true);
|
||||
}
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
@@ -210,7 +214,7 @@ export const QueryForm: React.FC<QueryFormProps> = ({query, setQuery, background
|
||||
<SyntaxHighlighter
|
||||
isWrapped={false}
|
||||
showLineNumbers={false}
|
||||
code={`timestamp < datetime("10/28/2021, 9:13:02 PM")`}
|
||||
code={`timestamp < datetime("10/28/2021, 9:13:02.905 PM")`}
|
||||
language="python"
|
||||
/>
|
||||
</Grid>
|
||||
@@ -226,7 +230,7 @@ export const QueryForm: React.FC<QueryFormProps> = ({query, setQuery, background
|
||||
language="python"
|
||||
/>
|
||||
<Typography id="modal-modal-description">
|
||||
By clicking the UI elements in both left-pane and right-pane, you can automatically select a field and update the query:
|
||||
By clicking the plus icon that appears beside the queryable UI elements on hovering in both left-pane and right-pane, you can automatically select a field and update the query:
|
||||
</Typography>
|
||||
<img
|
||||
src={filterUIExample1}
|
||||
@@ -235,12 +239,12 @@ export const QueryForm: React.FC<QueryFormProps> = ({query, setQuery, background
|
||||
title="Clicking to UI elements (left-pane)"
|
||||
/>
|
||||
<Typography id="modal-modal-description">
|
||||
Such that; clicking this in left-pane, would append the query below:
|
||||
Such that; clicking this icon in left-pane, would append the query below:
|
||||
</Typography>
|
||||
<SyntaxHighlighter
|
||||
isWrapped={false}
|
||||
showLineNumbers={false}
|
||||
code={`and service == "http://carts.sock-shop"`}
|
||||
code={`and dst.name == "carts.sock-shop"`}
|
||||
language="python"
|
||||
/>
|
||||
<Typography id="modal-modal-description">
|
||||
@@ -301,7 +305,7 @@ export const QueryForm: React.FC<QueryFormProps> = ({query, setQuery, background
|
||||
<SyntaxHighlighter
|
||||
isWrapped={false}
|
||||
showLineNumbers={false}
|
||||
code={`timestamp >= datetime("10/19/2021, 6:29:02 PM")`}
|
||||
code={`timestamp >= datetime("10/19/2021, 6:29:02.593 PM")`}
|
||||
language="python"
|
||||
/>
|
||||
<Typography id="modal-modal-description">
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, {useEffect, useRef, useState} from "react";
|
||||
import {Filters} from "./Filters";
|
||||
import {EntriesList} from "./EntriesList";
|
||||
import {EntryItem} from "./EntryListItem/EntryListItem";
|
||||
import {makeStyles} from "@material-ui/core";
|
||||
import "./style/TrafficPage.sass";
|
||||
import styles from './style/EntriesList.module.sass';
|
||||
@@ -21,7 +20,7 @@ const useLayoutStyles = makeStyles(() => ({
|
||||
padding: "12px 24px",
|
||||
borderRadius: 4,
|
||||
marginTop: 15,
|
||||
background: variables.headerBackgoundColor,
|
||||
background: variables.headerBackgroundColor,
|
||||
},
|
||||
|
||||
viewer: {
|
||||
@@ -51,11 +50,12 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({setAnalyzeStatus, onTLS
|
||||
const classes = useLayoutStyles();
|
||||
|
||||
const [entries, setEntries] = useState([] as any);
|
||||
const [entriesBuffer, setEntriesBuffer] = useState([] as any);
|
||||
const [focusedEntryId, setFocusedEntryId] = useState(null);
|
||||
const [selectedEntryData, setSelectedEntryData] = useState(null);
|
||||
const [connection, setConnection] = useState(ConnectionStatus.Closed);
|
||||
|
||||
const [noMoreDataTop, setNoMoreDataTop] = useState(false);
|
||||
|
||||
const [tappingStatus, setTappingStatus] = useState(null);
|
||||
|
||||
const [isSnappedToBottom, setIsSnappedToBottom] = useState(true);
|
||||
@@ -66,6 +66,8 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({setAnalyzeStatus, onTLS
|
||||
|
||||
const [queriedCurrent, setQueriedCurrent] = useState(0);
|
||||
const [queriedTotal, setQueriedTotal] = useState(0);
|
||||
const [leftOffBottom, setLeftOffBottom] = useState(0);
|
||||
const [leftOffTop, setLeftOffTop] = useState(null);
|
||||
|
||||
const [startTime, setStartTime] = useState(0);
|
||||
|
||||
@@ -100,16 +102,30 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({setAnalyzeStatus, onTLS
|
||||
|
||||
const listEntry = useRef(null);
|
||||
|
||||
const openWebSocket = (query) => {
|
||||
setFocusedEntryId(null);
|
||||
setEntries([]);
|
||||
setEntriesBuffer([]);
|
||||
const openWebSocket = (query: string, resetEntries: boolean) => {
|
||||
if (resetEntries) {
|
||||
setFocusedEntryId(null);
|
||||
setEntries([]);
|
||||
setQueriedCurrent(0);
|
||||
setLeftOffTop(null);
|
||||
setNoMoreDataTop(false);
|
||||
}
|
||||
ws.current = new WebSocket(MizuWebsocketURL);
|
||||
ws.current.onopen = () => {
|
||||
ws.current.send(query)
|
||||
setConnection(ConnectionStatus.Connected);
|
||||
ws.current.send(query);
|
||||
}
|
||||
ws.current.onclose = () => {
|
||||
setConnection(ConnectionStatus.Closed);
|
||||
}
|
||||
ws.current.onerror = (event) => {
|
||||
console.error("WebSocket error:", event);
|
||||
if (query) {
|
||||
openWebSocket(`(${query}) and leftOff(${leftOffBottom})`, false);
|
||||
} else {
|
||||
openWebSocket(`leftOff(${leftOffBottom})`, false);
|
||||
}
|
||||
}
|
||||
ws.current.onclose = () => setConnection(ConnectionStatus.Closed);
|
||||
}
|
||||
|
||||
if (ws.current) {
|
||||
@@ -119,17 +135,14 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({setAnalyzeStatus, onTLS
|
||||
switch (message.messageType) {
|
||||
case "entry":
|
||||
const entry = message.data;
|
||||
if (!focusedEntryId) setFocusedEntryId(entry.id.toString());
|
||||
setEntriesBuffer([
|
||||
...entriesBuffer,
|
||||
<EntryItem
|
||||
key={entry.id}
|
||||
entry={entry}
|
||||
setFocusedEntryId={setFocusedEntryId}
|
||||
style={{}}
|
||||
updateQuery={updateQuery}
|
||||
/>
|
||||
]);
|
||||
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);
|
||||
@@ -153,9 +166,12 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({setAnalyzeStatus, onTLS
|
||||
});
|
||||
break;
|
||||
case "queryMetadata":
|
||||
setQueriedCurrent(message.data.current);
|
||||
setQueriedCurrent(queriedCurrent + message.data.current);
|
||||
setQueriedTotal(message.data.total);
|
||||
setEntries(entriesBuffer);
|
||||
setLeftOffBottom(message.data.leftOff);
|
||||
if (leftOffTop === null) {
|
||||
setLeftOffTop(message.data.leftOff - 1);
|
||||
}
|
||||
break;
|
||||
case "startTime":
|
||||
setStartTime(message.data);
|
||||
@@ -168,7 +184,7 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({setAnalyzeStatus, onTLS
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
openWebSocket("rlimit(100)");
|
||||
openWebSocket("leftOff(-1)", true);
|
||||
try{
|
||||
const tapStatusResponse = await api.tapStatus();
|
||||
setTappingStatus(tapStatusResponse);
|
||||
@@ -190,27 +206,33 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({setAnalyzeStatus, onTLS
|
||||
const entryData = await api.getEntry(focusedEntryId);
|
||||
setSelectedEntryData(entryData);
|
||||
} catch (error) {
|
||||
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,
|
||||
});
|
||||
if (error.response) {
|
||||
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);
|
||||
}
|
||||
})()
|
||||
}, [focusedEntryId])
|
||||
})();
|
||||
// eslint-disable-next-line
|
||||
}, [focusedEntryId]);
|
||||
|
||||
const toggleConnection = () => {
|
||||
if (connection === ConnectionStatus.Connected) {
|
||||
ws.current.close();
|
||||
} else {
|
||||
openWebSocket(query);
|
||||
setConnection(ConnectionStatus.Connected);
|
||||
if (query) {
|
||||
openWebSocket(`(${query}) and leftOff(${leftOffBottom})`, false);
|
||||
} else {
|
||||
openWebSocket(`leftOff(${leftOffBottom})`, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +256,10 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({setAnalyzeStatus, onTLS
|
||||
}
|
||||
|
||||
const onSnapBrokenEvent = () => {
|
||||
setIsSnappedToBottom(false)
|
||||
setIsSnappedToBottom(false);
|
||||
if (connection === ConnectionStatus.Connected) {
|
||||
ws.current.close();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -263,13 +288,27 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({setAnalyzeStatus, onTLS
|
||||
<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}
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import styles from './style/Protocol.module.sass';
|
||||
import Queryable from "./Queryable";
|
||||
|
||||
export interface ProtocolInterface {
|
||||
name: string
|
||||
@@ -22,34 +23,46 @@ interface ProtocolProps {
|
||||
|
||||
const Protocol: React.FC<ProtocolProps> = ({protocol, horizontal, updateQuery}) => {
|
||||
if (horizontal) {
|
||||
return <a target="_blank" rel="noopener noreferrer" href={protocol.referenceLink}>
|
||||
return <Queryable
|
||||
query={protocol.macro}
|
||||
updateQuery={updateQuery}
|
||||
displayIconOnMouseOver={true}
|
||||
>
|
||||
<a target="_blank" rel="noopener noreferrer" href={protocol.referenceLink}>
|
||||
<span
|
||||
className={`${styles.base} ${styles.horizontal}`}
|
||||
style={{
|
||||
backgroundColor: protocol.backgroundColor,
|
||||
color: protocol.foregroundColor,
|
||||
fontSize: 13,
|
||||
}}
|
||||
title={protocol.abbr}
|
||||
>
|
||||
{protocol.longName}
|
||||
</span>
|
||||
</a>
|
||||
</Queryable>
|
||||
} else {
|
||||
return <Queryable
|
||||
query={protocol.macro}
|
||||
updateQuery={updateQuery}
|
||||
displayIconOnMouseOver={true}
|
||||
flipped={false}
|
||||
iconStyle={{marginTop: "52px", marginRight: "10px", zIndex: 1000}}
|
||||
>
|
||||
<span
|
||||
className={`${styles.base} ${styles.horizontal}`}
|
||||
className={`${styles.base} ${styles.vertical}`}
|
||||
style={{
|
||||
backgroundColor: protocol.backgroundColor,
|
||||
color: protocol.foregroundColor,
|
||||
fontSize: 13,
|
||||
fontSize: protocol.fontSize,
|
||||
marginRight: "-20px",
|
||||
}}
|
||||
title={protocol.abbr}
|
||||
title={protocol.longName}
|
||||
>
|
||||
{protocol.longName}
|
||||
{protocol.abbr}
|
||||
</span>
|
||||
</a>
|
||||
} else {
|
||||
return <span
|
||||
className={`${styles.base} ${styles.vertical}`}
|
||||
style={{
|
||||
backgroundColor: protocol.backgroundColor,
|
||||
color: protocol.foregroundColor,
|
||||
fontSize: protocol.fontSize,
|
||||
}}
|
||||
title={protocol.longName}
|
||||
onClick={() => {
|
||||
updateQuery(protocol.macro)
|
||||
}}
|
||||
>
|
||||
{protocol.abbr}
|
||||
</span>
|
||||
</Queryable>
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
62
ui/src/components/UI/Queryable.tsx
Normal file
62
ui/src/components/UI/Queryable.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||
import AddCircleIcon from '@material-ui/icons/AddCircle';
|
||||
import './style/Queryable.sass';
|
||||
|
||||
interface Props {
|
||||
query: string,
|
||||
updateQuery: any,
|
||||
style?: object,
|
||||
iconStyle?: object,
|
||||
className?: string,
|
||||
useTooltip?: boolean,
|
||||
displayIconOnMouseOver?: boolean,
|
||||
flipped?: boolean,
|
||||
}
|
||||
|
||||
const Queryable: React.FC<Props> = ({query, updateQuery, style, iconStyle, className, useTooltip= true, displayIconOnMouseOver = false, flipped = false, children}) => {
|
||||
const [showAddedNotification, setAdded] = useState(false);
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
|
||||
const onCopy = () => {
|
||||
setAdded(true)
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let timer;
|
||||
if (showAddedNotification) {
|
||||
updateQuery(query);
|
||||
timer = setTimeout(() => {
|
||||
setAdded(false);
|
||||
}, 1000);
|
||||
}
|
||||
return () => clearTimeout(timer);
|
||||
}, [showAddedNotification, query, updateQuery]);
|
||||
|
||||
const addButton = query ? <CopyToClipboard text={query} onCopy={onCopy}>
|
||||
<span
|
||||
className={`Queryable-Icon`}
|
||||
title={`Add "${query}" to the filter`}
|
||||
style={iconStyle}
|
||||
>
|
||||
<AddCircleIcon fontSize="small" color="inherit"></AddCircleIcon>
|
||||
{showAddedNotification && <span className={'Queryable-AddNotifier'}>Added</span>}
|
||||
</span>
|
||||
</CopyToClipboard> : null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`Queryable-Container displayIconOnMouseOver ${className ? className : ''} ${displayIconOnMouseOver ? 'displayIconOnMouseOver ' : ''}`}
|
||||
style={style}
|
||||
onMouseOver={ e => setShowTooltip(true)}
|
||||
onMouseLeave={ e => setShowTooltip(false)}
|
||||
>
|
||||
{flipped && addButton}
|
||||
{children}
|
||||
{!flipped && addButton}
|
||||
{useTooltip && showTooltip && <span className={'Queryable-Tooltip'}>{query}</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Queryable;
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import styles from './style/StatusCode.module.sass';
|
||||
import Queryable from "./Queryable";
|
||||
|
||||
export enum StatusCodeClassification {
|
||||
SUCCESS = "success",
|
||||
@@ -16,15 +17,20 @@ const StatusCode: React.FC<EntryProps> = ({statusCode, updateQuery}) => {
|
||||
|
||||
const classification = getClassification(statusCode)
|
||||
|
||||
return <span
|
||||
title="Status Code"
|
||||
className={`queryable ${styles[classification]} ${styles.base}`}
|
||||
onClick={() => {
|
||||
updateQuery(`response.status == ${statusCode}`)
|
||||
}}
|
||||
return <Queryable
|
||||
query={`response.status == ${statusCode}`}
|
||||
updateQuery={updateQuery}
|
||||
displayIconOnMouseOver={true}
|
||||
flipped={true}
|
||||
iconStyle={{marginTop: "40px", paddingLeft: "10px"}}
|
||||
>
|
||||
{statusCode}
|
||||
</span>
|
||||
<span
|
||||
title="Status Code"
|
||||
className={`${styles[classification]} ${styles.base}`}
|
||||
>
|
||||
{statusCode}
|
||||
</span>
|
||||
</Queryable>
|
||||
};
|
||||
|
||||
export function getClassification(statusCode: number): string {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import miscStyles from "./style/misc.module.sass";
|
||||
import React from "react";
|
||||
import styles from './style/Summary.module.sass';
|
||||
import Queryable from "./Queryable";
|
||||
|
||||
interface SummaryProps {
|
||||
method: string
|
||||
@@ -9,24 +10,29 @@ interface SummaryProps {
|
||||
}
|
||||
|
||||
export const Summary: React.FC<SummaryProps> = ({method, summary, updateQuery}) => {
|
||||
|
||||
return <div className={styles.container}>
|
||||
{method && <span
|
||||
title="Method"
|
||||
className={`queryable ${miscStyles.protocol} ${miscStyles.method}`}
|
||||
onClick={() => {
|
||||
updateQuery(`method == "${method}"`)
|
||||
}}
|
||||
{method && <Queryable
|
||||
query={`method == "${method}"`}
|
||||
className={`${miscStyles.protocol} ${miscStyles.method}`}
|
||||
updateQuery={updateQuery}
|
||||
displayIconOnMouseOver={true}
|
||||
style={{whiteSpace: "nowrap"}}
|
||||
>
|
||||
{method}
|
||||
</span>}
|
||||
{summary && <div
|
||||
title="Summary"
|
||||
className={`queryable ${styles.summary}`}
|
||||
onClick={() => {
|
||||
updateQuery(`summary == "${summary}"`)
|
||||
}}
|
||||
<span>
|
||||
{method}
|
||||
</span>
|
||||
</Queryable>}
|
||||
{summary && <Queryable
|
||||
query={`summary == "${summary}"`}
|
||||
updateQuery={updateQuery}
|
||||
displayIconOnMouseOver={true}
|
||||
>
|
||||
{summary}
|
||||
</div>}
|
||||
<div
|
||||
className={`${styles.summary}`}
|
||||
>
|
||||
{summary}
|
||||
</div>
|
||||
</Queryable>}
|
||||
</div>
|
||||
};
|
||||
|
||||
48
ui/src/components/UI/style/Queryable.sass
Normal file
48
ui/src/components/UI/style/Queryable.sass
Normal file
@@ -0,0 +1,48 @@
|
||||
.Queryable-Container
|
||||
display: flex
|
||||
align-items: center
|
||||
|
||||
&.displayIconOnMouseOver
|
||||
.Queryable-Icon
|
||||
opacity: 0
|
||||
width: 0px
|
||||
pointer-events: none
|
||||
&:hover
|
||||
.Queryable-Icon
|
||||
opacity: 1
|
||||
pointer-events: all
|
||||
|
||||
|
||||
.Queryable-Icon
|
||||
height: 22px
|
||||
width: 22px
|
||||
cursor: pointer
|
||||
color: #27AE60
|
||||
|
||||
&:hover
|
||||
background-color: rgba(255, 255, 255, 0.06)
|
||||
border-radius: 4px
|
||||
color: #1E884B
|
||||
|
||||
.Queryable-AddNotifier
|
||||
background-color: #1E884B
|
||||
font-weight: normal
|
||||
padding: 2px 5px
|
||||
border-radius: 4px
|
||||
position: absolute
|
||||
transform: translate(0, 10%)
|
||||
color: white
|
||||
z-index: 1000
|
||||
font-size: 11px
|
||||
|
||||
.Queryable-Tooltip
|
||||
background-color: #1E884B
|
||||
font-weight: normal
|
||||
padding: 2px 5px
|
||||
border-radius: 4px
|
||||
position: absolute
|
||||
transform: translate(0, -80%)
|
||||
color: white
|
||||
z-index: 1000
|
||||
font-size: 11px
|
||||
|
||||
@@ -3,6 +3,4 @@
|
||||
align-items: center
|
||||
|
||||
.summary
|
||||
text-overflow: ellipsis
|
||||
overflow: hidden
|
||||
white-space: nowrap
|
||||
|
||||
@@ -11,13 +11,14 @@
|
||||
|
||||
&.method
|
||||
margin-right: 10px
|
||||
height: 12px
|
||||
|
||||
&.filterPlate
|
||||
border-color: #bcc6dd20
|
||||
color: #a0b2ff
|
||||
font-size: 10px
|
||||
|
||||
.noSelect
|
||||
.noSelect
|
||||
-webkit-touch-callout: none
|
||||
-webkit-user-select: none
|
||||
-khtml-user-select: none
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 46 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 24 KiB |
@@ -38,6 +38,31 @@
|
||||
border: 1px solid #627ef7
|
||||
background-color: rgba(255, 255, 255, 0.06)
|
||||
|
||||
.spinnerContainer
|
||||
display: flex
|
||||
justify-content: center
|
||||
margin-bottom: 10px
|
||||
|
||||
.noMoreDataAvailable
|
||||
text-align: center
|
||||
font-weight: 600
|
||||
color: $secondary-font-color
|
||||
|
||||
.btnOld
|
||||
position: absolute
|
||||
top: 20px
|
||||
right: 10px
|
||||
background: #205CF5
|
||||
border-radius: 50%
|
||||
height: 35px
|
||||
width: 35px
|
||||
border: none
|
||||
cursor: pointer
|
||||
z-index: 1
|
||||
img
|
||||
height: 10px
|
||||
transform: scaleY(-1)
|
||||
|
||||
.btnLive
|
||||
position: absolute
|
||||
bottom: 10px
|
||||
|
||||
@@ -114,4 +114,4 @@
|
||||
.playPauseIcon
|
||||
cursor: pointer
|
||||
margin-right: 15px
|
||||
height: 30px
|
||||
height: 30px
|
||||
|
||||
@@ -38,6 +38,14 @@ export default class Api {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
fetchEntries = async (leftOff, direction, query, limit, timeoutMs) => {
|
||||
const response = await this.client.get(`/entries/?leftOff=${leftOff}&direction=${direction}&query=${query}&limit=${limit}&timeoutMs=${timeoutMs}`).catch(function (thrown) {
|
||||
console.error(thrown.message);
|
||||
return {};
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
getRecentTLSLinks = async () => {
|
||||
const response = await this.client.get("/status/recentTLSLinks");
|
||||
return response.data;
|
||||
|
||||
@@ -22,11 +22,6 @@ code
|
||||
.uppercase
|
||||
text-transform: uppercase
|
||||
|
||||
.queryable
|
||||
cursor: pointer
|
||||
&:hover
|
||||
text-decoration: underline
|
||||
|
||||
/****
|
||||
* Button
|
||||
***/
|
||||
|
||||
@@ -11,7 +11,7 @@ $blue-gray: #494677;
|
||||
|
||||
:export {
|
||||
mainBackgroundColor: $main-background-color;
|
||||
headerBackgoundColor: $header-background-color;
|
||||
headerBackgroundColor: $header-background-color;
|
||||
fontColor: $font-color;
|
||||
secondaryFontColor: $secondary-font-color;
|
||||
blueColor: $blue-color;
|
||||
|
||||
Reference in New Issue
Block a user