Compare commits
11 Commits
35.0-dev7
...
35.0-dev18
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13ed8eb58a | ||
|
|
48619b3e1c | ||
|
|
3b0b311e1e | ||
|
|
3a9236a381 | ||
|
|
2e7fd34210 | ||
|
|
01af6aa19c | ||
|
|
2bfae1baae | ||
|
|
2df9fb49db | ||
|
|
c1b2cda468 | ||
|
|
cf91f3dab4 | ||
|
|
1ebc51b45e |
@@ -54,14 +54,14 @@ ENV BPF_TARGET=arm64 BPF_CFLAGS="-O2 -g -D__TARGET_ARCH_arm64"
|
|||||||
|
|
||||||
|
|
||||||
### Builder image for x86-64 to AArch64 cross-compilation
|
### Builder image for x86-64 to AArch64 cross-compilation
|
||||||
FROM up9inc/linux-arm64-musl-go-libpcap-capstone-bpf AS builder-from-amd64-to-arm64v8
|
FROM up9inc/linux-arm64-musl-go-libpcap-capstone-bpf:capstone-5.0-rc2 AS builder-from-amd64-to-arm64v8
|
||||||
ENV CGO_ENABLED=1 GOOS=linux
|
ENV CGO_ENABLED=1 GOOS=linux
|
||||||
ENV GOARCH=arm64 CGO_CFLAGS="-I/work/libpcap -I/work/capstone/include"
|
ENV GOARCH=arm64 CGO_CFLAGS="-I/work/libpcap -I/work/capstone/include"
|
||||||
ENV BPF_TARGET=arm64 BPF_CFLAGS="-O2 -g -D__TARGET_ARCH_arm64 -I/usr/xcc/aarch64-linux-musl-cross/aarch64-linux-musl/include/"
|
ENV BPF_TARGET=arm64 BPF_CFLAGS="-O2 -g -D__TARGET_ARCH_arm64 -I/usr/xcc/aarch64-linux-musl-cross/aarch64-linux-musl/include/"
|
||||||
|
|
||||||
|
|
||||||
### Builder image for AArch64 to x86-64 cross-compilation
|
### Builder image for AArch64 to x86-64 cross-compilation
|
||||||
FROM up9inc/linux-x86_64-musl-go-libpcap-capstone-bpf AS builder-from-arm64v8-to-amd64
|
FROM up9inc/linux-x86_64-musl-go-libpcap-capstone-bpf:capstone-5.0-rc2 AS builder-from-arm64v8-to-amd64
|
||||||
ENV CGO_ENABLED=1 GOOS=linux
|
ENV CGO_ENABLED=1 GOOS=linux
|
||||||
ENV GOARCH=amd64 CGO_CFLAGS="-I/libpcap -I/capstone/include"
|
ENV GOARCH=amd64 CGO_CFLAGS="-I/libpcap -I/capstone/include"
|
||||||
ENV BPF_TARGET=amd64 BPF_CFLAGS="-O2 -g -D__TARGET_ARCH_x86 -I/usr/local/musl/x86_64-unknown-linux-musl/include/"
|
ENV BPF_TARGET=amd64 BPF_CFLAGS="-O2 -g -D__TARGET_ARCH_x86 -I/usr/local/musl/x86_64-unknown-linux-musl/include/"
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ require (
|
|||||||
github.com/antelman107/net-wait-go v0.0.0-20210623112055-cf684aebda7b
|
github.com/antelman107/net-wait-go v0.0.0-20210623112055-cf684aebda7b
|
||||||
github.com/chanced/openapi v0.0.8
|
github.com/chanced/openapi v0.0.8
|
||||||
github.com/djherbis/atime v1.1.0
|
github.com/djherbis/atime v1.1.0
|
||||||
github.com/getkin/kin-openapi v0.89.0
|
|
||||||
github.com/gin-contrib/pprof v1.3.0
|
github.com/gin-contrib/pprof v1.3.0
|
||||||
github.com/gin-contrib/static v0.0.1
|
github.com/gin-contrib/static v0.0.1
|
||||||
github.com/gin-gonic/gin v1.7.7
|
github.com/gin-gonic/gin v1.7.7
|
||||||
@@ -59,7 +58,6 @@ require (
|
|||||||
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
|
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
|
||||||
github.com/fatih/camelcase v1.0.0 // indirect
|
github.com/fatih/camelcase v1.0.0 // indirect
|
||||||
github.com/fvbommel/sortorder v1.0.2 // indirect
|
github.com/fvbommel/sortorder v1.0.2 // indirect
|
||||||
github.com/ghodss/yaml v1.0.0 // indirect
|
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
github.com/go-errors/errors v1.4.2 // indirect
|
github.com/go-errors/errors v1.4.2 // indirect
|
||||||
github.com/go-logr/logr v1.2.2 // indirect
|
github.com/go-logr/logr v1.2.2 // indirect
|
||||||
@@ -85,7 +83,7 @@ require (
|
|||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/compress v1.14.2 // indirect
|
github.com/klauspost/compress v1.14.2 // indirect
|
||||||
github.com/knightsc/gapstone v0.0.0-20211014144438-5e0e64002a6e // indirect
|
github.com/knightsc/gapstone v0.0.0-20191231144527-6fa5afaf11a9 // indirect
|
||||||
github.com/leodido/go-urn v1.2.1 // indirect
|
github.com/leodido/go-urn v1.2.1 // indirect
|
||||||
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
|
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
|
||||||
github.com/mailru/easyjson v0.7.7 // indirect
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
|
|||||||
@@ -211,9 +211,6 @@ github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui72
|
|||||||
github.com/fvbommel/sortorder v1.0.2 h1:mV4o8B2hKboCdkJm+a7uX/SIpZob4JzUpc5GGnM45eo=
|
github.com/fvbommel/sortorder v1.0.2 h1:mV4o8B2hKboCdkJm+a7uX/SIpZob4JzUpc5GGnM45eo=
|
||||||
github.com/fvbommel/sortorder v1.0.2/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
|
github.com/fvbommel/sortorder v1.0.2/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
|
||||||
github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg=
|
github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg=
|
||||||
github.com/getkin/kin-openapi v0.89.0 h1:p4nagHchUKGn85z/f+pse4aSh50nIBOYjOhMIku2hiA=
|
|
||||||
github.com/getkin/kin-openapi v0.89.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg=
|
|
||||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
|
||||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
github.com/gin-contrib/pprof v1.3.0 h1:G9eK6HnbkSqDZBYbzG4wrjCsA4e+cvYAHUZw6W+W9K0=
|
github.com/gin-contrib/pprof v1.3.0 h1:G9eK6HnbkSqDZBYbzG4wrjCsA4e+cvYAHUZw6W+W9K0=
|
||||||
github.com/gin-contrib/pprof v1.3.0/go.mod h1:waMjT1H9b179t3CxuG1cV3DHpga6ybizwfBaM5OXaB0=
|
github.com/gin-contrib/pprof v1.3.0/go.mod h1:waMjT1H9b179t3CxuG1cV3DHpga6ybizwfBaM5OXaB0=
|
||||||
@@ -373,7 +370,6 @@ github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2c
|
|||||||
github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=
|
github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=
|
||||||
github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA=
|
github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
|
||||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||||
@@ -457,8 +453,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
|
|||||||
github.com/klauspost/compress v1.9.8/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
github.com/klauspost/compress v1.9.8/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||||
github.com/klauspost/compress v1.14.2 h1:S0OHlFk/Gbon/yauFJ4FfJJF5V0fc5HbBTJazi28pRw=
|
github.com/klauspost/compress v1.14.2 h1:S0OHlFk/Gbon/yauFJ4FfJJF5V0fc5HbBTJazi28pRw=
|
||||||
github.com/klauspost/compress v1.14.2/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
github.com/klauspost/compress v1.14.2/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||||
github.com/knightsc/gapstone v0.0.0-20211014144438-5e0e64002a6e h1:6J5obSn9umEThiYzWzndcPOZR0Qj/sVCZpH6V1G7yNE=
|
github.com/knightsc/gapstone v0.0.0-20191231144527-6fa5afaf11a9 h1:1KszOoXSFt0aRQ6wxxcKm7QKgfLPI0TWO47UcY/f+vA=
|
||||||
github.com/knightsc/gapstone v0.0.0-20211014144438-5e0e64002a6e/go.mod h1:1K5hEzsMBLTPdRJKEHqBFJ8Zt2VRqDhomcQ11KH0WW4=
|
github.com/knightsc/gapstone v0.0.0-20191231144527-6fa5afaf11a9/go.mod h1:1K5hEzsMBLTPdRJKEHqBFJ8Zt2VRqDhomcQ11KH0WW4=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ func hostApi(socketHarOutputChannel chan<- *tapApi.OutputChannelItem) *gin.Engin
|
|||||||
routes.MetadataRoutes(ginApp)
|
routes.MetadataRoutes(ginApp)
|
||||||
routes.StatusRoutes(ginApp)
|
routes.StatusRoutes(ginApp)
|
||||||
routes.DbRoutes(ginApp)
|
routes.DbRoutes(ginApp)
|
||||||
|
routes.ReplayRoutes(ginApp)
|
||||||
|
|
||||||
return ginApp
|
return ginApp
|
||||||
}
|
}
|
||||||
@@ -155,7 +156,7 @@ func runInTapperMode() {
|
|||||||
|
|
||||||
hostMode := os.Getenv(shared.HostModeEnvVar) == "1"
|
hostMode := os.Getenv(shared.HostModeEnvVar) == "1"
|
||||||
tapOpts := &tap.TapOpts{
|
tapOpts := &tap.TapOpts{
|
||||||
HostMode: hostMode,
|
HostMode: hostMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
filteredOutputItemsChannel := make(chan *tapApi.OutputChannelItem)
|
filteredOutputItemsChannel := make(chan *tapApi.OutputChannelItem)
|
||||||
|
|||||||
@@ -1,107 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/getkin/kin-openapi/openapi3"
|
|
||||||
"github.com/getkin/kin-openapi/openapi3filter"
|
|
||||||
"github.com/getkin/kin-openapi/routers"
|
|
||||||
legacyrouter "github.com/getkin/kin-openapi/routers/legacy"
|
|
||||||
|
|
||||||
"github.com/up9inc/mizu/shared"
|
|
||||||
"github.com/up9inc/mizu/tap/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
ContractNotApplicable api.ContractStatus = 0
|
|
||||||
ContractPassed api.ContractStatus = 1
|
|
||||||
ContractFailed api.ContractStatus = 2
|
|
||||||
)
|
|
||||||
|
|
||||||
func loadOAS(ctx context.Context) (doc *openapi3.T, contractContent string, router routers.Router, err error) {
|
|
||||||
path := fmt.Sprintf("%s%s", shared.ConfigDirPath, shared.ContractFileName)
|
|
||||||
bytesValue, err := ioutil.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
contractContent = string(bytesValue)
|
|
||||||
loader := &openapi3.Loader{Context: ctx}
|
|
||||||
doc, _ = loader.LoadFromData(bytesValue)
|
|
||||||
err = doc.Validate(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
router, _ = legacyrouter.NewRouter(doc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateOAS(ctx context.Context, doc *openapi3.T, router routers.Router, req *http.Request, res *http.Response) (isValid bool, reqErr error, resErr error) {
|
|
||||||
isValid = true
|
|
||||||
reqErr = nil
|
|
||||||
resErr = nil
|
|
||||||
|
|
||||||
// Find route
|
|
||||||
route, pathParams, err := router.FindRoute(req)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate request
|
|
||||||
requestValidationInput := &openapi3filter.RequestValidationInput{
|
|
||||||
Request: req,
|
|
||||||
PathParams: pathParams,
|
|
||||||
Route: route,
|
|
||||||
}
|
|
||||||
if reqErr = openapi3filter.ValidateRequest(ctx, requestValidationInput); reqErr != nil {
|
|
||||||
isValid = false
|
|
||||||
}
|
|
||||||
|
|
||||||
responseValidationInput := &openapi3filter.ResponseValidationInput{
|
|
||||||
RequestValidationInput: requestValidationInput,
|
|
||||||
Status: res.StatusCode,
|
|
||||||
Header: res.Header,
|
|
||||||
}
|
|
||||||
|
|
||||||
if res.Body != nil {
|
|
||||||
body, _ := ioutil.ReadAll(res.Body)
|
|
||||||
res.Body = ioutil.NopCloser(bytes.NewBuffer(body))
|
|
||||||
responseValidationInput.SetBodyBytes(body)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate response.
|
|
||||||
if resErr = openapi3filter.ValidateResponse(ctx, responseValidationInput); resErr != nil {
|
|
||||||
isValid = false
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleOAS(ctx context.Context, doc *openapi3.T, router routers.Router, req *http.Request, res *http.Response, contractContent string) (contract api.Contract) {
|
|
||||||
contract = api.Contract{
|
|
||||||
Content: contractContent,
|
|
||||||
Status: ContractNotApplicable,
|
|
||||||
}
|
|
||||||
|
|
||||||
isValid, reqErr, resErr := validateOAS(ctx, doc, router, req, res)
|
|
||||||
if isValid {
|
|
||||||
contract.Status = ContractPassed
|
|
||||||
} else {
|
|
||||||
contract.Status = ContractFailed
|
|
||||||
if reqErr != nil {
|
|
||||||
contract.RequestReason = reqErr.Error()
|
|
||||||
} else {
|
|
||||||
contract.RequestReason = ""
|
|
||||||
}
|
|
||||||
if resErr != nil {
|
|
||||||
contract.ResponseReason = resErr.Error()
|
|
||||||
} else {
|
|
||||||
contract.ResponseReason = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
@@ -22,7 +22,16 @@ func (e *DefaultEntryStreamerSocketConnector) SendEntry(socketId int, entry *tap
|
|||||||
if params.EnableFullEntries {
|
if params.EnableFullEntries {
|
||||||
message, _ = models.CreateFullEntryWebSocketMessage(entry)
|
message, _ = models.CreateFullEntryWebSocketMessage(entry)
|
||||||
} else {
|
} else {
|
||||||
extension := extensionsMap[entry.Protocol.Name]
|
protocol, ok := protocolsMap[entry.ProtocolId]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("protocol not found, protocol: %v", protocol)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension, ok := extensionsMap[protocol.Name]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("extension not found, extension: %v", protocol.Name)
|
||||||
|
}
|
||||||
|
|
||||||
base := extension.Dissector.Summarize(entry)
|
base := extension.Dissector.Summarize(entry)
|
||||||
message, _ = models.CreateBaseEntryWebSocketMessage(base)
|
message, _ = models.CreateBaseEntryWebSocketMessage(base)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,14 +99,6 @@ func startReadingChannel(outputItems <-chan *tapApi.OutputChannelItem, extension
|
|||||||
panic("Channel of captured messages is nil")
|
panic("Channel of captured messages is nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
disableOASValidation := false
|
|
||||||
ctx := context.Background()
|
|
||||||
doc, contractContent, router, err := loadOAS(ctx)
|
|
||||||
if err != nil {
|
|
||||||
logger.Log.Infof("Disabled OAS validation: %s", err.Error())
|
|
||||||
disableOASValidation = true
|
|
||||||
}
|
|
||||||
|
|
||||||
for item := range outputItems {
|
for item := range outputItems {
|
||||||
extension := extensionsMap[item.Protocol.Name]
|
extension := extensionsMap[item.Protocol.Name]
|
||||||
resolvedSource, resolvedDestionation, namespace := resolveIP(item.ConnectionInfo)
|
resolvedSource, resolvedDestionation, namespace := resolveIP(item.ConnectionInfo)
|
||||||
@@ -117,19 +109,6 @@ func startReadingChannel(outputItems <-chan *tapApi.OutputChannelItem, extension
|
|||||||
|
|
||||||
mizuEntry := extension.Dissector.Analyze(item, resolvedSource, resolvedDestionation, namespace)
|
mizuEntry := extension.Dissector.Analyze(item, resolvedSource, resolvedDestionation, namespace)
|
||||||
if extension.Protocol.Name == "http" {
|
if extension.Protocol.Name == "http" {
|
||||||
if !disableOASValidation {
|
|
||||||
var httpPair tapApi.HTTPRequestResponsePair
|
|
||||||
if err := json.Unmarshal([]byte(mizuEntry.HTTPPair), &httpPair); err != nil {
|
|
||||||
logger.Log.Error(err)
|
|
||||||
} else {
|
|
||||||
contract := handleOAS(ctx, doc, router, httpPair.Request.Payload.RawRequest, httpPair.Response.Payload.RawResponse, contractContent)
|
|
||||||
mizuEntry.ContractStatus = contract.Status
|
|
||||||
mizuEntry.ContractRequestReason = contract.RequestReason
|
|
||||||
mizuEntry.ContractResponseReason = contract.ResponseReason
|
|
||||||
mizuEntry.ContractContent = contract.Content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
harEntry, err := har.NewEntry(mizuEntry.Request, mizuEntry.Response, mizuEntry.StartTime, mizuEntry.ElapsedTime)
|
harEntry, err := har.NewEntry(mizuEntry.Request, mizuEntry.Response, mizuEntry.StartTime, mizuEntry.ElapsedTime)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
rules, _, _ := models.RunValidationRulesState(*harEntry, mizuEntry.Destination.Name)
|
rules, _, _ := models.RunValidationRulesState(*harEntry, mizuEntry.Destination.Name)
|
||||||
@@ -155,7 +134,7 @@ func startReadingChannel(outputItems <-chan *tapApi.OutputChannelItem, extension
|
|||||||
serviceMapGenerator.NewTCPEntry(mizuEntry.Source, mizuEntry.Destination, &item.Protocol)
|
serviceMapGenerator.NewTCPEntry(mizuEntry.Source, mizuEntry.Destination, &item.Protocol)
|
||||||
|
|
||||||
oasGenerator := dependency.GetInstance(dependency.OasGeneratorDependency).(oas.OasGeneratorSink)
|
oasGenerator := dependency.GetInstance(dependency.OasGeneratorDependency).(oas.OasGeneratorSink)
|
||||||
oasGenerator.HandleEntry(mizuEntry)
|
oasGenerator.HandleEntry(mizuEntry, &item.Protocol)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,10 +14,14 @@ import (
|
|||||||
tapApi "github.com/up9inc/mizu/tap/api"
|
tapApi "github.com/up9inc/mizu/tap/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
var extensionsMap map[string]*tapApi.Extension // global
|
var (
|
||||||
|
extensionsMap map[string]*tapApi.Extension // global
|
||||||
|
protocolsMap map[string]*tapApi.Protocol //global
|
||||||
|
)
|
||||||
|
|
||||||
func InitExtensionsMap(ref map[string]*tapApi.Extension) {
|
func InitMaps(extensions map[string]*tapApi.Extension, protocols map[string]*tapApi.Protocol) {
|
||||||
extensionsMap = ref
|
extensionsMap = extensions
|
||||||
|
protocolsMap = protocols
|
||||||
}
|
}
|
||||||
|
|
||||||
type EventHandlers interface {
|
type EventHandlers interface {
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import (
|
|||||||
"github.com/up9inc/mizu/agent/pkg/api"
|
"github.com/up9inc/mizu/agent/pkg/api"
|
||||||
"github.com/up9inc/mizu/agent/pkg/utils"
|
"github.com/up9inc/mizu/agent/pkg/utils"
|
||||||
"github.com/up9inc/mizu/logger"
|
"github.com/up9inc/mizu/logger"
|
||||||
"github.com/up9inc/mizu/tap/dbgctl"
|
|
||||||
tapApi "github.com/up9inc/mizu/tap/api"
|
tapApi "github.com/up9inc/mizu/tap/api"
|
||||||
|
"github.com/up9inc/mizu/tap/dbgctl"
|
||||||
amqpExt "github.com/up9inc/mizu/tap/extensions/amqp"
|
amqpExt "github.com/up9inc/mizu/tap/extensions/amqp"
|
||||||
httpExt "github.com/up9inc/mizu/tap/extensions/http"
|
httpExt "github.com/up9inc/mizu/tap/extensions/http"
|
||||||
kafkaExt "github.com/up9inc/mizu/tap/extensions/kafka"
|
kafkaExt "github.com/up9inc/mizu/tap/extensions/kafka"
|
||||||
@@ -22,11 +22,13 @@ import (
|
|||||||
var (
|
var (
|
||||||
Extensions []*tapApi.Extension // global
|
Extensions []*tapApi.Extension // global
|
||||||
ExtensionsMap map[string]*tapApi.Extension // global
|
ExtensionsMap map[string]*tapApi.Extension // global
|
||||||
|
ProtocolsMap map[string]*tapApi.Protocol //global
|
||||||
)
|
)
|
||||||
|
|
||||||
func LoadExtensions() {
|
func LoadExtensions() {
|
||||||
Extensions = make([]*tapApi.Extension, 0)
|
Extensions = make([]*tapApi.Extension, 0)
|
||||||
ExtensionsMap = make(map[string]*tapApi.Extension)
|
ExtensionsMap = make(map[string]*tapApi.Extension)
|
||||||
|
ProtocolsMap = make(map[string]*tapApi.Protocol)
|
||||||
|
|
||||||
extensionHttp := &tapApi.Extension{}
|
extensionHttp := &tapApi.Extension{}
|
||||||
dissectorHttp := httpExt.NewDissector()
|
dissectorHttp := httpExt.NewDissector()
|
||||||
@@ -34,6 +36,10 @@ func LoadExtensions() {
|
|||||||
extensionHttp.Dissector = dissectorHttp
|
extensionHttp.Dissector = dissectorHttp
|
||||||
Extensions = append(Extensions, extensionHttp)
|
Extensions = append(Extensions, extensionHttp)
|
||||||
ExtensionsMap[extensionHttp.Protocol.Name] = extensionHttp
|
ExtensionsMap[extensionHttp.Protocol.Name] = extensionHttp
|
||||||
|
protocolsHttp := dissectorHttp.GetProtocols()
|
||||||
|
for k, v := range protocolsHttp {
|
||||||
|
ProtocolsMap[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
if !dbgctl.MizuTapperDisableNonHttpExtensions {
|
if !dbgctl.MizuTapperDisableNonHttpExtensions {
|
||||||
extensionAmqp := &tapApi.Extension{}
|
extensionAmqp := &tapApi.Extension{}
|
||||||
@@ -42,6 +48,10 @@ func LoadExtensions() {
|
|||||||
extensionAmqp.Dissector = dissectorAmqp
|
extensionAmqp.Dissector = dissectorAmqp
|
||||||
Extensions = append(Extensions, extensionAmqp)
|
Extensions = append(Extensions, extensionAmqp)
|
||||||
ExtensionsMap[extensionAmqp.Protocol.Name] = extensionAmqp
|
ExtensionsMap[extensionAmqp.Protocol.Name] = extensionAmqp
|
||||||
|
protocolsAmqp := dissectorAmqp.GetProtocols()
|
||||||
|
for k, v := range protocolsAmqp {
|
||||||
|
ProtocolsMap[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
extensionKafka := &tapApi.Extension{}
|
extensionKafka := &tapApi.Extension{}
|
||||||
dissectorKafka := kafkaExt.NewDissector()
|
dissectorKafka := kafkaExt.NewDissector()
|
||||||
@@ -49,6 +59,10 @@ func LoadExtensions() {
|
|||||||
extensionKafka.Dissector = dissectorKafka
|
extensionKafka.Dissector = dissectorKafka
|
||||||
Extensions = append(Extensions, extensionKafka)
|
Extensions = append(Extensions, extensionKafka)
|
||||||
ExtensionsMap[extensionKafka.Protocol.Name] = extensionKafka
|
ExtensionsMap[extensionKafka.Protocol.Name] = extensionKafka
|
||||||
|
protocolsKafka := dissectorKafka.GetProtocols()
|
||||||
|
for k, v := range protocolsKafka {
|
||||||
|
ProtocolsMap[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
extensionRedis := &tapApi.Extension{}
|
extensionRedis := &tapApi.Extension{}
|
||||||
dissectorRedis := redisExt.NewDissector()
|
dissectorRedis := redisExt.NewDissector()
|
||||||
@@ -56,13 +70,17 @@ func LoadExtensions() {
|
|||||||
extensionRedis.Dissector = dissectorRedis
|
extensionRedis.Dissector = dissectorRedis
|
||||||
Extensions = append(Extensions, extensionRedis)
|
Extensions = append(Extensions, extensionRedis)
|
||||||
ExtensionsMap[extensionRedis.Protocol.Name] = extensionRedis
|
ExtensionsMap[extensionRedis.Protocol.Name] = extensionRedis
|
||||||
|
protocolsRedis := dissectorRedis.GetProtocols()
|
||||||
|
for k, v := range protocolsRedis {
|
||||||
|
ProtocolsMap[k] = v
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Slice(Extensions, func(i, j int) bool {
|
sort.Slice(Extensions, func(i, j int) bool {
|
||||||
return Extensions[i].Protocol.Priority < Extensions[j].Protocol.Priority
|
return Extensions[i].Protocol.Priority < Extensions[j].Protocol.Priority
|
||||||
})
|
})
|
||||||
|
|
||||||
api.InitExtensionsMap(ExtensionsMap)
|
api.InitMaps(ExtensionsMap, ProtocolsMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConfigureBasenineServer(host string, port string, dbSize int64, logLevel logging.Level, insertionFilter string) {
|
func ConfigureBasenineServer(host string, port string, dbSize int64, logLevel logging.Level, insertionFilter string) {
|
||||||
|
|||||||
34
agent/pkg/controllers/replay_controller.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/up9inc/mizu/agent/pkg/replay"
|
||||||
|
"github.com/up9inc/mizu/agent/pkg/validation"
|
||||||
|
"github.com/up9inc/mizu/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
replayTimeout = 10 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
func ReplayRequest(c *gin.Context) {
|
||||||
|
logger.Log.Debug("Starting replay")
|
||||||
|
replayDetails := &replay.Details{}
|
||||||
|
if err := c.Bind(replayDetails); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Log.Debugf("Validating replay, %v", replayDetails)
|
||||||
|
if err := validation.Validate(replayDetails); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Log.Debug("Executing replay, %v", replayDetails)
|
||||||
|
result := replay.ExecuteRequest(replayDetails, replayTimeout)
|
||||||
|
c.JSON(http.StatusOK, result)
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package entries
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
basenine "github.com/up9inc/basenine/client/go"
|
basenine "github.com/up9inc/basenine/client/go"
|
||||||
@@ -38,11 +39,20 @@ func (e *BasenineEntriesProvider) GetEntries(entriesRequest *models.EntriesReque
|
|||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
extension := app.ExtensionsMap[entry.Protocol.Name]
|
protocol, ok := app.ProtocolsMap[entry.ProtocolId]
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, fmt.Errorf("protocol not found, protocol: %v", protocol)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension, ok := app.ExtensionsMap[protocol.Name]
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, fmt.Errorf("extension not found, extension: %v", protocol.Name)
|
||||||
|
}
|
||||||
|
|
||||||
base := extension.Dissector.Summarize(entry)
|
base := extension.Dissector.Summarize(entry)
|
||||||
|
|
||||||
dataSlice = append(dataSlice, &tapApi.EntryWrapper{
|
dataSlice = append(dataSlice, &tapApi.EntryWrapper{
|
||||||
Protocol: entry.Protocol,
|
Protocol: *protocol,
|
||||||
Data: entry,
|
Data: entry,
|
||||||
Base: base,
|
Base: base,
|
||||||
})
|
})
|
||||||
@@ -68,7 +78,16 @@ func (e *BasenineEntriesProvider) GetEntry(singleEntryRequest *models.SingleEntr
|
|||||||
return nil, errors.New(string(bytes))
|
return nil, errors.New(string(bytes))
|
||||||
}
|
}
|
||||||
|
|
||||||
extension := app.ExtensionsMap[entry.Protocol.Name]
|
protocol, ok := app.ProtocolsMap[entry.ProtocolId]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("protocol not found, protocol: %v", protocol)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension, ok := app.ExtensionsMap[protocol.Name]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("extension not found, extension: %v", protocol.Name)
|
||||||
|
}
|
||||||
|
|
||||||
base := extension.Dissector.Summarize(entry)
|
base := extension.Dissector.Summarize(entry)
|
||||||
var representation []byte
|
var representation []byte
|
||||||
representation, err = extension.Dissector.Represent(entry.Request, entry.Response)
|
representation, err = extension.Dissector.Represent(entry.Request, entry.Response)
|
||||||
@@ -78,7 +97,7 @@ func (e *BasenineEntriesProvider) GetEntry(singleEntryRequest *models.SingleEntr
|
|||||||
|
|
||||||
var rules []map[string]interface{}
|
var rules []map[string]interface{}
|
||||||
var isRulesEnabled bool
|
var isRulesEnabled bool
|
||||||
if entry.Protocol.Name == "http" {
|
if protocol.Name == "http" {
|
||||||
harEntry, _ := har.NewEntry(entry.Request, entry.Response, entry.StartTime, entry.ElapsedTime)
|
harEntry, _ := har.NewEntry(entry.Request, entry.Response, entry.StartTime, entry.ElapsedTime)
|
||||||
_, rulesMatched, _isRulesEnabled := models.RunValidationRulesState(*harEntry, entry.Destination.Name)
|
_, rulesMatched, _isRulesEnabled := models.RunValidationRulesState(*harEntry, entry.Destination.Name)
|
||||||
isRulesEnabled = _isRulesEnabled
|
isRulesEnabled = _isRulesEnabled
|
||||||
@@ -89,7 +108,7 @@ func (e *BasenineEntriesProvider) GetEntry(singleEntryRequest *models.SingleEntr
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &tapApi.EntryWrapper{
|
return &tapApi.EntryWrapper{
|
||||||
Protocol: entry.Protocol,
|
Protocol: *protocol,
|
||||||
Representation: string(representation),
|
Representation: string(representation),
|
||||||
Data: entry,
|
Data: entry,
|
||||||
Base: base,
|
Base: base,
|
||||||
|
|||||||
@@ -6,9 +6,8 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/up9inc/mizu/agent/pkg/har"
|
"github.com/up9inc/mizu/agent/pkg/har"
|
||||||
"github.com/up9inc/mizu/tap/api"
|
|
||||||
|
|
||||||
"github.com/up9inc/mizu/logger"
|
"github.com/up9inc/mizu/logger"
|
||||||
|
"github.com/up9inc/mizu/tap/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -17,7 +16,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type OasGeneratorSink interface {
|
type OasGeneratorSink interface {
|
||||||
HandleEntry(mizuEntry *api.Entry)
|
HandleEntry(mizuEntry *api.Entry, protocol *api.Protocol)
|
||||||
}
|
}
|
||||||
|
|
||||||
type OasGenerator interface {
|
type OasGenerator interface {
|
||||||
@@ -59,12 +58,12 @@ func (g *defaultOasGenerator) IsStarted() bool {
|
|||||||
return g.started
|
return g.started
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *defaultOasGenerator) HandleEntry(mizuEntry *api.Entry) {
|
func (g *defaultOasGenerator) HandleEntry(mizuEntry *api.Entry, protocol *api.Protocol) {
|
||||||
if !g.started {
|
if !g.started {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if mizuEntry.Protocol.Name == "http" {
|
if protocol.Name == "http" {
|
||||||
dest := mizuEntry.Destination.Name
|
dest := mizuEntry.Destination.Name
|
||||||
if dest == "" {
|
if dest == "" {
|
||||||
logger.Log.Debugf("OAS: Unresolved entry %d", mizuEntry.Id)
|
logger.Log.Debugf("OAS: Unresolved entry %d", mizuEntry.Id)
|
||||||
@@ -86,7 +85,7 @@ func (g *defaultOasGenerator) HandleEntry(mizuEntry *api.Entry) {
|
|||||||
|
|
||||||
g.handleHARWithSource(entryWSource)
|
g.handleHARWithSource(entryWSource)
|
||||||
} else {
|
} else {
|
||||||
logger.Log.Debugf("OAS: Unsupported protocol in entry %d: %s", mizuEntry.Id, mizuEntry.Protocol.Name)
|
logger.Log.Debugf("OAS: Unsupported protocol in entry %d: %s", mizuEntry.Id, protocol.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ func GetTappedPodsStatus() []shared.TappedPodStatus {
|
|||||||
|
|
||||||
func SetNodeToTappedPodMap(nodeToTappedPodsMap shared.NodeToPodsMap) {
|
func SetNodeToTappedPodMap(nodeToTappedPodsMap shared.NodeToPodsMap) {
|
||||||
summary := nodeToTappedPodsMap.Summary()
|
summary := nodeToTappedPodsMap.Summary()
|
||||||
logger.Log.Infof("Setting node to tapped pods map to %v", summary)
|
logger.Log.Debugf("Setting node to tapped pods map to %v", summary)
|
||||||
|
|
||||||
nodeHostToTappedPodsMap = nodeToTappedPodsMap
|
nodeHostToTappedPodsMap = nodeToTappedPodsMap
|
||||||
}
|
}
|
||||||
|
|||||||
186
agent/pkg/replay/replay.go
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
package replay
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/up9inc/mizu/agent/pkg/app"
|
||||||
|
tapApi "github.com/up9inc/mizu/tap/api"
|
||||||
|
mizuhttp "github.com/up9inc/mizu/tap/extensions/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
inProcessRequestsLocker = sync.Mutex{}
|
||||||
|
inProcessRequests = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
const maxParallelAction = 5
|
||||||
|
|
||||||
|
type Details struct {
|
||||||
|
Method string `json:"method"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
Headers map[string]string `json:"headers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
Success bool `json:"status"`
|
||||||
|
Data interface{} `json:"data"`
|
||||||
|
ErrorMessage string `json:"errorMessage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func incrementCounter() bool {
|
||||||
|
result := false
|
||||||
|
inProcessRequestsLocker.Lock()
|
||||||
|
if inProcessRequests < maxParallelAction {
|
||||||
|
inProcessRequests++
|
||||||
|
result = true
|
||||||
|
}
|
||||||
|
inProcessRequestsLocker.Unlock()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func decrementCounter() {
|
||||||
|
inProcessRequestsLocker.Lock()
|
||||||
|
inProcessRequests--
|
||||||
|
inProcessRequestsLocker.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEntryFromRequestResponse(extension *tapApi.Extension, request *http.Request, response *http.Response) *tapApi.Entry {
|
||||||
|
captureTime := time.Now()
|
||||||
|
|
||||||
|
itemTmp := tapApi.OutputChannelItem{
|
||||||
|
Protocol: *extension.Protocol,
|
||||||
|
ConnectionInfo: &tapApi.ConnectionInfo{
|
||||||
|
ClientIP: "",
|
||||||
|
ClientPort: "1",
|
||||||
|
ServerIP: "",
|
||||||
|
ServerPort: "1",
|
||||||
|
IsOutgoing: false,
|
||||||
|
},
|
||||||
|
Capture: "",
|
||||||
|
Timestamp: time.Now().UnixMilli(),
|
||||||
|
Pair: &tapApi.RequestResponsePair{
|
||||||
|
Request: tapApi.GenericMessage{
|
||||||
|
IsRequest: true,
|
||||||
|
CaptureTime: captureTime,
|
||||||
|
CaptureSize: 0,
|
||||||
|
Payload: &mizuhttp.HTTPPayload{
|
||||||
|
Type: mizuhttp.TypeHttpRequest,
|
||||||
|
Data: request,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Response: tapApi.GenericMessage{
|
||||||
|
IsRequest: false,
|
||||||
|
CaptureTime: captureTime,
|
||||||
|
CaptureSize: 0,
|
||||||
|
Payload: &mizuhttp.HTTPPayload{
|
||||||
|
Type: mizuhttp.TypeHttpResponse,
|
||||||
|
Data: response,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Analyze is expecting an item that's marshalled and unmarshalled
|
||||||
|
itemMarshalled, err := json.Marshal(itemTmp)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var finalItem *tapApi.OutputChannelItem
|
||||||
|
if err := json.Unmarshal(itemMarshalled, &finalItem); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return extension.Dissector.Analyze(finalItem, "", "", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExecuteRequest(replayData *Details, timeout time.Duration) *Response {
|
||||||
|
if incrementCounter() {
|
||||||
|
defer decrementCounter()
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: timeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
request, err := http.NewRequest(strings.ToUpper(replayData.Method), replayData.Url, bytes.NewBufferString(replayData.Body))
|
||||||
|
if err != nil {
|
||||||
|
return &Response{
|
||||||
|
Success: false,
|
||||||
|
Data: nil,
|
||||||
|
ErrorMessage: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for headerKey, headerValue := range replayData.Headers {
|
||||||
|
request.Header.Add(headerKey, headerValue)
|
||||||
|
}
|
||||||
|
request.Header.Add("x-mizu", uuid.New().String())
|
||||||
|
response, requestErr := client.Do(request)
|
||||||
|
|
||||||
|
if requestErr != nil {
|
||||||
|
return &Response{
|
||||||
|
Success: false,
|
||||||
|
Data: nil,
|
||||||
|
ErrorMessage: requestErr.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension := app.ExtensionsMap["http"] // # TODO: maybe pass the extension to the function so it can be tested
|
||||||
|
entry := getEntryFromRequestResponse(extension, request, response)
|
||||||
|
base := extension.Dissector.Summarize(entry)
|
||||||
|
var representation []byte
|
||||||
|
|
||||||
|
// Represent is expecting an entry that's marshalled and unmarshalled
|
||||||
|
entryMarshalled, err := json.Marshal(entry)
|
||||||
|
if err != nil {
|
||||||
|
return &Response{
|
||||||
|
Success: false,
|
||||||
|
Data: nil,
|
||||||
|
ErrorMessage: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var entryUnmarshalled *tapApi.Entry
|
||||||
|
if err := json.Unmarshal(entryMarshalled, &entryUnmarshalled); err != nil {
|
||||||
|
return &Response{
|
||||||
|
Success: false,
|
||||||
|
Data: nil,
|
||||||
|
ErrorMessage: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
representation, err = extension.Dissector.Represent(entryUnmarshalled.Request, entryUnmarshalled.Response)
|
||||||
|
if err != nil {
|
||||||
|
return &Response{
|
||||||
|
Success: false,
|
||||||
|
Data: nil,
|
||||||
|
ErrorMessage: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Response{
|
||||||
|
Success: true,
|
||||||
|
Data: &tapApi.EntryWrapper{
|
||||||
|
Protocol: *extension.Protocol,
|
||||||
|
Representation: string(representation),
|
||||||
|
Data: entryUnmarshalled,
|
||||||
|
Base: base,
|
||||||
|
Rules: nil,
|
||||||
|
IsRulesEnabled: false,
|
||||||
|
},
|
||||||
|
ErrorMessage: "",
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return &Response{
|
||||||
|
Success: false,
|
||||||
|
Data: nil,
|
||||||
|
ErrorMessage: fmt.Sprintf("reached threshold of %d requests", maxParallelAction),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
108
agent/pkg/replay/replay_internal_test.go
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
package replay
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
tapApi "github.com/up9inc/mizu/tap/api"
|
||||||
|
mizuhttp "github.com/up9inc/mizu/tap/extensions/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValid(t *testing.T) {
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := map[string]*Details{
|
||||||
|
"40x": {
|
||||||
|
Method: "GET",
|
||||||
|
Url: "http://httpbin.org/status/404",
|
||||||
|
Body: "",
|
||||||
|
Headers: map[string]string{},
|
||||||
|
},
|
||||||
|
"20x": {
|
||||||
|
Method: "GET",
|
||||||
|
Url: "http://httpbin.org/status/200",
|
||||||
|
Body: "",
|
||||||
|
Headers: map[string]string{},
|
||||||
|
},
|
||||||
|
"50x": {
|
||||||
|
Method: "GET",
|
||||||
|
Url: "http://httpbin.org/status/500",
|
||||||
|
Body: "",
|
||||||
|
Headers: map[string]string{},
|
||||||
|
},
|
||||||
|
// TODO: this should be fixes, currently not working because of header name with ":"
|
||||||
|
//":path-header": {
|
||||||
|
// Method: "GET",
|
||||||
|
// Url: "http://httpbin.org/get",
|
||||||
|
// Body: "",
|
||||||
|
// Headers: map[string]string{
|
||||||
|
// ":path": "/get",
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
}
|
||||||
|
|
||||||
|
for testCaseName, replayData := range tests {
|
||||||
|
t.Run(fmt.Sprintf("%+v", testCaseName), func(t *testing.T) {
|
||||||
|
request, err := http.NewRequest(strings.ToUpper(replayData.Method), replayData.Url, bytes.NewBufferString(replayData.Body))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error executing request")
|
||||||
|
}
|
||||||
|
|
||||||
|
for headerKey, headerValue := range replayData.Headers {
|
||||||
|
request.Header.Add(headerKey, headerValue)
|
||||||
|
}
|
||||||
|
request.Header.Add("x-mizu", uuid.New().String())
|
||||||
|
response, requestErr := client.Do(request)
|
||||||
|
|
||||||
|
if requestErr != nil {
|
||||||
|
t.Errorf("failed: %v, ", requestErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
extensionHttp := &tapApi.Extension{}
|
||||||
|
dissectorHttp := mizuhttp.NewDissector()
|
||||||
|
dissectorHttp.Register(extensionHttp)
|
||||||
|
extensionHttp.Dissector = dissectorHttp
|
||||||
|
extension := extensionHttp
|
||||||
|
|
||||||
|
entry := getEntryFromRequestResponse(extension, request, response)
|
||||||
|
base := extension.Dissector.Summarize(entry)
|
||||||
|
|
||||||
|
// Represent is expecting an entry that's marshalled and unmarshalled
|
||||||
|
entryMarshalled, err := json.Marshal(entry)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed marshaling entry: %v, ", err)
|
||||||
|
}
|
||||||
|
var entryUnmarshalled *tapApi.Entry
|
||||||
|
if err := json.Unmarshal(entryMarshalled, &entryUnmarshalled); err != nil {
|
||||||
|
t.Errorf("failed unmarshaling entry: %v, ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var representation []byte
|
||||||
|
representation, err = extension.Dissector.Represent(entryUnmarshalled.Request, entryUnmarshalled.Response)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed: %v, ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &tapApi.EntryWrapper{
|
||||||
|
Protocol: *extension.Protocol,
|
||||||
|
Representation: string(representation),
|
||||||
|
Data: entry,
|
||||||
|
Base: base,
|
||||||
|
Rules: nil,
|
||||||
|
IsRulesEnabled: false,
|
||||||
|
}
|
||||||
|
t.Logf("%+v", result)
|
||||||
|
//data, _ := json.MarshalIndent(result, "", " ")
|
||||||
|
//t.Logf("%+v", string(data))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
13
agent/pkg/routes/replay_routes.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/up9inc/mizu/agent/pkg/controllers"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReplayRoutes defines the group of replay routes.
|
||||||
|
func ReplayRoutes(app *gin.Engine) {
|
||||||
|
routeGroup := app.Group("/replay")
|
||||||
|
|
||||||
|
routeGroup.POST("/", controllers.ReplayRequest)
|
||||||
|
}
|
||||||
@@ -54,8 +54,8 @@ func init() {
|
|||||||
tapCmd.Flags().String(configStructs.InsertionFilterName, defaultTapConfig.InsertionFilter, "Set the insertion filter. Accepts string or a file path.")
|
tapCmd.Flags().String(configStructs.InsertionFilterName, defaultTapConfig.InsertionFilter, "Set the insertion filter. Accepts string or a file path.")
|
||||||
tapCmd.Flags().Bool(configStructs.DryRunTapName, defaultTapConfig.DryRun, "Preview of all pods matching the regex, without tapping them")
|
tapCmd.Flags().Bool(configStructs.DryRunTapName, defaultTapConfig.DryRun, "Preview of all pods matching the regex, without tapping them")
|
||||||
tapCmd.Flags().String(configStructs.EnforcePolicyFile, defaultTapConfig.EnforcePolicyFile, "Yaml file path with policy rules")
|
tapCmd.Flags().String(configStructs.EnforcePolicyFile, defaultTapConfig.EnforcePolicyFile, "Yaml file path with policy rules")
|
||||||
tapCmd.Flags().String(configStructs.ContractFile, defaultTapConfig.ContractFile, "OAS/Swagger file to validate to monitor the contracts")
|
|
||||||
tapCmd.Flags().Bool(configStructs.ServiceMeshName, defaultTapConfig.ServiceMesh, "Record decrypted traffic if the cluster is configured with a service mesh and with mtls")
|
tapCmd.Flags().Bool(configStructs.ServiceMeshName, defaultTapConfig.ServiceMesh, "Record decrypted traffic if the cluster is configured with a service mesh and with mtls")
|
||||||
tapCmd.Flags().Bool(configStructs.TlsName, defaultTapConfig.Tls, "Record tls traffic")
|
tapCmd.Flags().Bool(configStructs.TlsName, defaultTapConfig.Tls, "Record tls traffic")
|
||||||
tapCmd.Flags().Bool(configStructs.ProfilerName, defaultTapConfig.Profiler, "Run pprof server")
|
tapCmd.Flags().Bool(configStructs.ProfilerName, defaultTapConfig.Profiler, "Run pprof server")
|
||||||
|
tapCmd.Flags().Int(configStructs.MaxLiveStreamsName, defaultTapConfig.MaxLiveStreams, "Maximum live tcp streams to handle concurrently")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -13,7 +12,6 @@ import (
|
|||||||
"github.com/up9inc/mizu/cli/telemetry"
|
"github.com/up9inc/mizu/cli/telemetry"
|
||||||
"github.com/up9inc/mizu/cli/utils"
|
"github.com/up9inc/mizu/cli/utils"
|
||||||
|
|
||||||
"github.com/getkin/kin-openapi/openapi3"
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
core "k8s.io/api/core/v1"
|
core "k8s.io/api/core/v1"
|
||||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
@@ -57,30 +55,6 @@ func RunMizuTap() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read and validate the OAS file
|
|
||||||
var serializedContract string
|
|
||||||
if config.Config.Tap.ContractFile != "" {
|
|
||||||
bytes, err := ioutil.ReadFile(config.Config.Tap.ContractFile)
|
|
||||||
if err != nil {
|
|
||||||
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error reading contract file: %v", errormessage.FormatError(err)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
serializedContract = string(bytes)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
loader := &openapi3.Loader{Context: ctx}
|
|
||||||
doc, err := loader.LoadFromData(bytes)
|
|
||||||
if err != nil {
|
|
||||||
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error loading contract file: %v", errormessage.FormatError(err)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = doc.Validate(ctx)
|
|
||||||
if err != nil {
|
|
||||||
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error validating contract file: %v", errormessage.FormatError(err)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
kubernetesProvider, err := getKubernetesProviderForCli()
|
kubernetesProvider, err := getKubernetesProviderForCli()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@@ -124,7 +98,7 @@ func RunMizuTap() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.Log.Infof("Waiting for Mizu Agent to start...")
|
logger.Log.Infof("Waiting for Mizu Agent to start...")
|
||||||
if state.mizuServiceAccountExists, err = resources.CreateTapMizuResources(ctx, kubernetesProvider, serializedValidationRules, serializedContract, serializedMizuConfig, config.Config.IsNsRestrictedMode(), config.Config.MizuResourcesNamespace, config.Config.AgentImage, config.Config.Tap.MaxEntriesDBSizeBytes(), config.Config.Tap.ApiServerResources, config.Config.ImagePullPolicy(), config.Config.LogLevel(), config.Config.Tap.Profiler); err != nil {
|
if state.mizuServiceAccountExists, err = resources.CreateTapMizuResources(ctx, kubernetesProvider, serializedValidationRules, serializedMizuConfig, config.Config.IsNsRestrictedMode(), config.Config.MizuResourcesNamespace, config.Config.AgentImage, config.Config.Tap.MaxEntriesDBSizeBytes(), config.Config.Tap.ApiServerResources, config.Config.ImagePullPolicy(), config.Config.LogLevel(), config.Config.Tap.Profiler); err != nil {
|
||||||
var statusError *k8serrors.StatusError
|
var statusError *k8serrors.StatusError
|
||||||
if errors.As(err, &statusError) && (statusError.ErrStatus.Reason == metav1.StatusReasonAlreadyExists) {
|
if errors.As(err, &statusError) && (statusError.ErrStatus.Reason == metav1.StatusReasonAlreadyExists) {
|
||||||
logger.Log.Info("Mizu is already running in this namespace, change the `mizu-resources-namespace` configuration or run `mizu clean` to remove the currently running Mizu instance")
|
logger.Log.Info("Mizu is already running in this namespace, change the `mizu-resources-namespace` configuration or run `mizu clean` to remove the currently running Mizu instance")
|
||||||
@@ -202,6 +176,7 @@ func startTapperSyncer(ctx context.Context, cancel context.CancelFunc, provider
|
|||||||
MizuServiceAccountExists: state.mizuServiceAccountExists,
|
MizuServiceAccountExists: state.mizuServiceAccountExists,
|
||||||
ServiceMesh: config.Config.Tap.ServiceMesh,
|
ServiceMesh: config.Config.Tap.ServiceMesh,
|
||||||
Tls: config.Config.Tap.Tls,
|
Tls: config.Config.Tap.Tls,
|
||||||
|
MaxLiveStreams: config.Config.Tap.MaxLiveStreams,
|
||||||
}, startTime)
|
}, startTime)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ const (
|
|||||||
InsertionFilterName = "insertion-filter"
|
InsertionFilterName = "insertion-filter"
|
||||||
DryRunTapName = "dry-run"
|
DryRunTapName = "dry-run"
|
||||||
EnforcePolicyFile = "traffic-validation-file"
|
EnforcePolicyFile = "traffic-validation-file"
|
||||||
ContractFile = "contract"
|
|
||||||
ServiceMeshName = "service-mesh"
|
ServiceMeshName = "service-mesh"
|
||||||
TlsName = "tls"
|
TlsName = "tls"
|
||||||
ProfilerName = "profiler"
|
ProfilerName = "profiler"
|
||||||
|
MaxLiveStreamsName = "max-live-streams"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TapConfig struct {
|
type TapConfig struct {
|
||||||
@@ -43,12 +43,12 @@ type TapConfig struct {
|
|||||||
InsertionFilter string `yaml:"insertion-filter" default:""`
|
InsertionFilter string `yaml:"insertion-filter" default:""`
|
||||||
DryRun bool `yaml:"dry-run" default:"false"`
|
DryRun bool `yaml:"dry-run" default:"false"`
|
||||||
EnforcePolicyFile string `yaml:"traffic-validation-file"`
|
EnforcePolicyFile string `yaml:"traffic-validation-file"`
|
||||||
ContractFile string `yaml:"contract"`
|
|
||||||
ApiServerResources shared.Resources `yaml:"api-server-resources"`
|
ApiServerResources shared.Resources `yaml:"api-server-resources"`
|
||||||
TapperResources shared.Resources `yaml:"tapper-resources"`
|
TapperResources shared.Resources `yaml:"tapper-resources"`
|
||||||
ServiceMesh bool `yaml:"service-mesh" default:"false"`
|
ServiceMesh bool `yaml:"service-mesh" default:"false"`
|
||||||
Tls bool `yaml:"tls" default:"false"`
|
Tls bool `yaml:"tls" default:"false"`
|
||||||
Profiler bool `yaml:"profiler" default:"false"`
|
Profiler bool `yaml:"profiler" default:"false"`
|
||||||
|
MaxLiveStreams int `yaml:"max-live-streams" default:"500"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config *TapConfig) PodRegex() *regexp.Regexp {
|
func (config *TapConfig) PodRegex() *regexp.Regexp {
|
||||||
|
|||||||
@@ -5,16 +5,13 @@ go 1.17
|
|||||||
require (
|
require (
|
||||||
github.com/creasty/defaults v1.5.2
|
github.com/creasty/defaults v1.5.2
|
||||||
github.com/denisbrodbeck/machineid v1.0.1
|
github.com/denisbrodbeck/machineid v1.0.1
|
||||||
github.com/getkin/kin-openapi v0.89.0
|
|
||||||
github.com/google/go-github/v37 v37.0.0
|
github.com/google/go-github/v37 v37.0.0
|
||||||
github.com/google/uuid v1.3.0
|
|
||||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||||
github.com/spf13/cobra v1.3.0
|
github.com/spf13/cobra v1.3.0
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
github.com/up9inc/mizu/logger v0.0.0
|
github.com/up9inc/mizu/logger v0.0.0
|
||||||
github.com/up9inc/mizu/shared v0.0.0
|
github.com/up9inc/mizu/shared v0.0.0
|
||||||
github.com/up9inc/mizu/tap/api v0.0.0
|
github.com/up9inc/mizu/tap/api v0.0.0
|
||||||
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||||
k8s.io/api v0.23.3
|
k8s.io/api v0.23.3
|
||||||
k8s.io/apimachinery v0.23.3
|
k8s.io/apimachinery v0.23.3
|
||||||
@@ -39,7 +36,6 @@ require (
|
|||||||
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
|
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
|
||||||
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
|
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
|
||||||
github.com/fvbommel/sortorder v1.0.2 // indirect
|
github.com/fvbommel/sortorder v1.0.2 // indirect
|
||||||
github.com/ghodss/yaml v1.0.0 // indirect
|
|
||||||
github.com/go-errors/errors v1.4.2 // indirect
|
github.com/go-errors/errors v1.4.2 // indirect
|
||||||
github.com/go-logr/logr v1.2.2 // indirect
|
github.com/go-logr/logr v1.2.2 // indirect
|
||||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||||
@@ -52,8 +48,8 @@ require (
|
|||||||
github.com/google/go-cmp v0.5.7 // indirect
|
github.com/google/go-cmp v0.5.7 // indirect
|
||||||
github.com/google/go-querystring v1.1.0 // indirect
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
github.com/google/gofuzz v1.2.0 // indirect
|
github.com/google/gofuzz v1.2.0 // indirect
|
||||||
github.com/google/martian v2.1.0+incompatible // indirect
|
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||||
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
github.com/googleapis/gnostic v0.5.5 // indirect
|
github.com/googleapis/gnostic v0.5.5 // indirect
|
||||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
|
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
|
||||||
github.com/imdario/mergo v0.3.12 // indirect
|
github.com/imdario/mergo v0.3.12 // indirect
|
||||||
@@ -79,6 +75,7 @@ require (
|
|||||||
go.starlark.net v0.0.0-20220203230714-bb14e151c28f // indirect
|
go.starlark.net v0.0.0-20220203230714-bb14e151c28f // indirect
|
||||||
golang.org/x/crypto v0.0.0-20220208050332-20e1d8d225ab // indirect
|
golang.org/x/crypto v0.0.0-20220208050332-20e1d8d225ab // indirect
|
||||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect
|
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect
|
||||||
|
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
|
||||||
golang.org/x/sys v0.0.0-20220207234003-57398862261d // indirect
|
golang.org/x/sys v0.0.0-20220207234003-57398862261d // indirect
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||||
golang.org/x/text v0.3.7 // indirect
|
golang.org/x/text v0.3.7 // indirect
|
||||||
|
|||||||
@@ -190,9 +190,6 @@ github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui72
|
|||||||
github.com/fvbommel/sortorder v1.0.2 h1:mV4o8B2hKboCdkJm+a7uX/SIpZob4JzUpc5GGnM45eo=
|
github.com/fvbommel/sortorder v1.0.2 h1:mV4o8B2hKboCdkJm+a7uX/SIpZob4JzUpc5GGnM45eo=
|
||||||
github.com/fvbommel/sortorder v1.0.2/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
|
github.com/fvbommel/sortorder v1.0.2/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
|
||||||
github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg=
|
github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg=
|
||||||
github.com/getkin/kin-openapi v0.89.0 h1:p4nagHchUKGn85z/f+pse4aSh50nIBOYjOhMIku2hiA=
|
|
||||||
github.com/getkin/kin-openapi v0.89.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg=
|
|
||||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
|
||||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||||
@@ -294,7 +291,6 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
|
|||||||
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
|
|
||||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||||
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||||
|
|||||||
@@ -14,14 +14,14 @@ import (
|
|||||||
core "k8s.io/api/core/v1"
|
core "k8s.io/api/core/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
func CreateTapMizuResources(ctx context.Context, kubernetesProvider *kubernetes.Provider, serializedValidationRules string, serializedContract string, serializedMizuConfig string, isNsRestrictedMode bool, mizuResourcesNamespace string, agentImage string, maxEntriesDBSizeBytes int64, apiServerResources shared.Resources, imagePullPolicy core.PullPolicy, logLevel logging.Level, profiler bool) (bool, error) {
|
func CreateTapMizuResources(ctx context.Context, kubernetesProvider *kubernetes.Provider, serializedValidationRules string, serializedMizuConfig string, isNsRestrictedMode bool, mizuResourcesNamespace string, agentImage string, maxEntriesDBSizeBytes int64, apiServerResources shared.Resources, imagePullPolicy core.PullPolicy, logLevel logging.Level, profiler bool) (bool, error) {
|
||||||
if !isNsRestrictedMode {
|
if !isNsRestrictedMode {
|
||||||
if err := createMizuNamespace(ctx, kubernetesProvider, mizuResourcesNamespace); err != nil {
|
if err := createMizuNamespace(ctx, kubernetesProvider, mizuResourcesNamespace); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := createMizuConfigmap(ctx, kubernetesProvider, serializedValidationRules, serializedContract, serializedMizuConfig, mizuResourcesNamespace); err != nil {
|
if err := createMizuConfigmap(ctx, kubernetesProvider, serializedValidationRules, serializedMizuConfig, mizuResourcesNamespace); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,8 +71,8 @@ func createMizuNamespace(ctx context.Context, kubernetesProvider *kubernetes.Pro
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func createMizuConfigmap(ctx context.Context, kubernetesProvider *kubernetes.Provider, serializedValidationRules string, serializedContract string, serializedMizuConfig string, mizuResourcesNamespace string) error {
|
func createMizuConfigmap(ctx context.Context, kubernetesProvider *kubernetes.Provider, serializedValidationRules string, serializedMizuConfig string, mizuResourcesNamespace string) error {
|
||||||
err := kubernetesProvider.CreateConfigMap(ctx, mizuResourcesNamespace, kubernetes.ConfigMapName, serializedValidationRules, serializedContract, serializedMizuConfig)
|
err := kubernetesProvider.CreateConfigMap(ctx, mizuResourcesNamespace, kubernetes.ConfigMapName, serializedValidationRules, serializedMizuConfig)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ if (( $EUID != 0 )); then
|
|||||||
SUDO='sudo'
|
SUDO='sudo'
|
||||||
fi
|
fi
|
||||||
|
|
||||||
curl https://github.com/capstone-engine/capstone/archive/4.0.2.tar.gz -Lo ./capstone.tar.gz \
|
curl https://github.com/capstone-engine/capstone/releases/download/5.0-rc2/capstone-5.0-rc2.tar.xz -Lo ./capstone.tar.xz \
|
||||||
&& tar -xzf capstone.tar.gz && mv ./capstone-* ./capstone \
|
&& tar -xf capstone.tar.xz && mv ./capstone-* ./capstone \
|
||||||
&& cd capstone \
|
&& cd capstone \
|
||||||
&& CAPSTONE_ARCHS="aarch64 x86" ./make.sh \
|
&& CAPSTONE_ARCHS="aarch64 x86" ./make.sh \
|
||||||
&& $SUDO ./make.sh install
|
&& $SUDO ./make.sh install
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ RUN ./configure --host=arm && make \
|
|||||||
WORKDIR /work
|
WORKDIR /work
|
||||||
|
|
||||||
# Build and install Capstone from source
|
# Build and install Capstone from source
|
||||||
RUN curl https://github.com/capstone-engine/capstone/archive/4.0.2.tar.gz -Lo ./capstone.tar.gz \
|
RUN curl https://github.com/capstone-engine/capstone/releases/download/5.0-rc2/capstone-5.0-rc2.tar.xz -Lo ./capstone.tar.xz \
|
||||||
&& tar -xzf capstone.tar.gz && mv ./capstone-* ./capstone
|
&& tar -xf capstone.tar.xz && mv ./capstone-* ./capstone
|
||||||
WORKDIR /work/capstone
|
WORKDIR /work/capstone
|
||||||
RUN CAPSTONE_ARCHS="aarch64" CAPSTONE_STATIC=yes ./make.sh \
|
RUN CAPSTONE_ARCHS="aarch64" CAPSTONE_STATIC=yes ./make.sh \
|
||||||
&& cp /work/capstone/libcapstone.a /usr/xcc/aarch64-linux-musl-cross/lib/gcc/aarch64-linux-musl/*/
|
&& cp /work/capstone/libcapstone.a /usr/xcc/aarch64-linux-musl-cross/lib/gcc/aarch64-linux-musl/*/
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
docker build . -t up9inc/linux-arm64-musl-go-libpcap-capstone-bpf && docker push up9inc/linux-arm64-musl-go-libpcap-capstone-bpf
|
# Build it on x86_64
|
||||||
|
docker build . -t up9inc/linux-arm64-musl-go-libpcap-capstone-bpf:capstone-5.0-rc2 && docker push up9inc/linux-arm64-musl-go-libpcap-capstone-bpf:capstone-5.0-rc2
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ RUN ./configure --host=x86_64 && make \
|
|||||||
WORKDIR /
|
WORKDIR /
|
||||||
|
|
||||||
# Build and install Capstone from source
|
# Build and install Capstone from source
|
||||||
RUN curl https://github.com/capstone-engine/capstone/archive/4.0.2.tar.gz -Lo ./capstone.tar.gz \
|
RUN curl https://github.com/capstone-engine/capstone/releases/download/5.0-rc2/capstone-5.0-rc2.tar.xz -Lo ./capstone.tar.xz \
|
||||||
&& tar -xzf capstone.tar.gz && mv ./capstone-* ./capstone
|
&& tar -xf capstone.tar.xz && mv ./capstone-* ./capstone
|
||||||
WORKDIR /capstone
|
WORKDIR /capstone
|
||||||
RUN CAPSTONE_ARCHS="x86" CAPSTONE_STATIC=yes ./make.sh \
|
RUN CAPSTONE_ARCHS="x86" CAPSTONE_STATIC=yes ./make.sh \
|
||||||
&& cp /capstone/libcapstone.a /usr/local/musl/lib/gcc/x86_64-unknown-linux-musl/*/
|
&& cp /capstone/libcapstone.a /usr/local/musl/lib/gcc/x86_64-unknown-linux-musl/*/
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
docker build . -t up9inc/linux-x86_64-musl-go-libpcap-capstone-bpf && docker push up9inc/linux-x86_64-musl-go-libpcap-capstone-bpf
|
# Build it on arm64
|
||||||
|
docker build . -t up9inc/linux-x86_64-musl-go-libpcap-capstone-bpf:capstone-5.0-rc2 && docker push up9inc/linux-x86_64-musl-go-libpcap-capstone-bpf:capstone-5.0-rc2
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ const (
|
|||||||
ConfigDirPath = "/app/config/"
|
ConfigDirPath = "/app/config/"
|
||||||
DataDirPath = "/app/data/"
|
DataDirPath = "/app/data/"
|
||||||
ValidationRulesFileName = "validation-rules.yaml"
|
ValidationRulesFileName = "validation-rules.yaml"
|
||||||
ContractFileName = "contract-oas.yaml"
|
|
||||||
ConfigFileName = "mizu-config.json"
|
ConfigFileName = "mizu-config.json"
|
||||||
DefaultApiServerPort = 8899
|
DefaultApiServerPort = 8899
|
||||||
LogLevelEnvVar = "LOG_LEVEL"
|
LogLevelEnvVar = "LOG_LEVEL"
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ type TapperSyncerConfig struct {
|
|||||||
MizuServiceAccountExists bool
|
MizuServiceAccountExists bool
|
||||||
ServiceMesh bool
|
ServiceMesh bool
|
||||||
Tls bool
|
Tls bool
|
||||||
|
MaxLiveStreams int
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateAndStartMizuTapperSyncer(ctx context.Context, kubernetesProvider *Provider, config TapperSyncerConfig, startTime time.Time) (*MizuTapperSyncer, error) {
|
func CreateAndStartMizuTapperSyncer(ctx context.Context, kubernetesProvider *Provider, config TapperSyncerConfig, startTime time.Time) (*MizuTapperSyncer, error) {
|
||||||
@@ -337,7 +338,8 @@ func (tapperSyncer *MizuTapperSyncer) updateMizuTappers() error {
|
|||||||
tapperSyncer.config.MizuApiFilteringOptions,
|
tapperSyncer.config.MizuApiFilteringOptions,
|
||||||
tapperSyncer.config.LogLevel,
|
tapperSyncer.config.LogLevel,
|
||||||
tapperSyncer.config.ServiceMesh,
|
tapperSyncer.config.ServiceMesh,
|
||||||
tapperSyncer.config.Tls); err != nil {
|
tapperSyncer.config.Tls,
|
||||||
|
tapperSyncer.config.MaxLiveStreams); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/op/go-logging"
|
"github.com/op/go-logging"
|
||||||
"github.com/up9inc/mizu/logger"
|
"github.com/up9inc/mizu/logger"
|
||||||
@@ -382,11 +383,11 @@ func (provider *Provider) GetMizuApiServerPodObject(opts *ApiServerOptions, moun
|
|||||||
Tolerations: []core.Toleration{
|
Tolerations: []core.Toleration{
|
||||||
{
|
{
|
||||||
Operator: core.TolerationOpExists,
|
Operator: core.TolerationOpExists,
|
||||||
Effect: core.TaintEffectNoExecute,
|
Effect: core.TaintEffectNoExecute,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Operator: core.TolerationOpExists,
|
Operator: core.TolerationOpExists,
|
||||||
Effect: core.TaintEffectNoSchedule,
|
Effect: core.TaintEffectNoSchedule,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -684,14 +685,11 @@ func (provider *Provider) handleRemovalError(err error) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *Provider) CreateConfigMap(ctx context.Context, namespace string, configMapName string, serializedValidationRules string, serializedContract string, serializedMizuConfig string) error {
|
func (provider *Provider) CreateConfigMap(ctx context.Context, namespace string, configMapName string, serializedValidationRules string, serializedMizuConfig string) error {
|
||||||
configMapData := make(map[string]string)
|
configMapData := make(map[string]string)
|
||||||
if serializedValidationRules != "" {
|
if serializedValidationRules != "" {
|
||||||
configMapData[shared.ValidationRulesFileName] = serializedValidationRules
|
configMapData[shared.ValidationRulesFileName] = serializedValidationRules
|
||||||
}
|
}
|
||||||
if serializedContract != "" {
|
|
||||||
configMapData[shared.ContractFileName] = serializedContract
|
|
||||||
}
|
|
||||||
configMapData[shared.ConfigFileName] = serializedMizuConfig
|
configMapData[shared.ConfigFileName] = serializedMizuConfig
|
||||||
|
|
||||||
configMap := &core.ConfigMap{
|
configMap := &core.ConfigMap{
|
||||||
@@ -714,7 +712,7 @@ func (provider *Provider) CreateConfigMap(ctx context.Context, namespace string,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *Provider) ApplyMizuTapperDaemonSet(ctx context.Context, namespace string, daemonSetName string, podImage string, tapperPodName string, apiServerPodIp string, nodeNames []string, serviceAccountName string, resources shared.Resources, imagePullPolicy core.PullPolicy, mizuApiFilteringOptions api.TrafficFilteringOptions, logLevel logging.Level, serviceMesh bool, tls bool) error {
|
func (provider *Provider) ApplyMizuTapperDaemonSet(ctx context.Context, namespace string, daemonSetName string, podImage string, tapperPodName string, apiServerPodIp string, nodeNames []string, serviceAccountName string, resources shared.Resources, imagePullPolicy core.PullPolicy, mizuApiFilteringOptions api.TrafficFilteringOptions, logLevel logging.Level, serviceMesh bool, tls bool, maxLiveStreams int) error {
|
||||||
logger.Log.Debugf("Applying %d tapper daemon sets, ns: %s, daemonSetName: %s, podImage: %s, tapperPodName: %s", len(nodeNames), namespace, daemonSetName, podImage, tapperPodName)
|
logger.Log.Debugf("Applying %d tapper daemon sets, ns: %s, daemonSetName: %s, podImage: %s, tapperPodName: %s", len(nodeNames), namespace, daemonSetName, podImage, tapperPodName)
|
||||||
|
|
||||||
if len(nodeNames) == 0 {
|
if len(nodeNames) == 0 {
|
||||||
@@ -732,6 +730,7 @@ func (provider *Provider) ApplyMizuTapperDaemonSet(ctx context.Context, namespac
|
|||||||
"--tap",
|
"--tap",
|
||||||
"--api-server-address", fmt.Sprintf("ws://%s/wsTapper", apiServerPodIp),
|
"--api-server-address", fmt.Sprintf("ws://%s/wsTapper", apiServerPodIp),
|
||||||
"--nodefrag",
|
"--nodefrag",
|
||||||
|
"--max-live-streams", strconv.Itoa(maxLiveStreams),
|
||||||
}
|
}
|
||||||
|
|
||||||
if serviceMesh {
|
if serviceMesh {
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ type WebSocketMessageMetadata struct {
|
|||||||
MessageType WebSocketMessageType `json:"messageType,omitempty"`
|
MessageType WebSocketMessageType `json:"messageType,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type WebSocketStatusMessage struct {
|
type WebSocketStatusMessage struct {
|
||||||
*WebSocketMessageMetadata
|
*WebSocketMessageMetadata
|
||||||
TappingStatus []TappedPodStatus `json:"tappingStatus"`
|
TappingStatus []TappedPodStatus `json:"tappingStatus"`
|
||||||
|
|||||||
252
tap/api/api.go
@@ -2,24 +2,13 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"sort"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/martian/har"
|
|
||||||
|
|
||||||
"github.com/up9inc/mizu/tap/dbgctl"
|
"github.com/up9inc/mizu/tap/dbgctl"
|
||||||
)
|
)
|
||||||
|
|
||||||
const mizuTestEnvVar = "MIZU_TEST"
|
|
||||||
const UnknownNamespace = ""
|
const UnknownNamespace = ""
|
||||||
|
|
||||||
var UnknownIp = net.IP{0, 0, 0, 0}
|
var UnknownIp = net.IP{0, 0, 0, 0}
|
||||||
@@ -102,7 +91,6 @@ type OutputChannelItem struct {
|
|||||||
Timestamp int64
|
Timestamp int64
|
||||||
ConnectionInfo *ConnectionInfo
|
ConnectionInfo *ConnectionInfo
|
||||||
Pair *RequestResponsePair
|
Pair *RequestResponsePair
|
||||||
Summary *BaseEntry
|
|
||||||
Namespace string
|
Namespace string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,6 +115,7 @@ func (p *ReadProgress) Reset() {
|
|||||||
|
|
||||||
type Dissector interface {
|
type Dissector interface {
|
||||||
Register(*Extension)
|
Register(*Extension)
|
||||||
|
GetProtocols() map[string]*Protocol
|
||||||
Ping()
|
Ping()
|
||||||
Dissect(b *bufio.Reader, reader TcpReader, options *TrafficFilteringOptions) error
|
Dissect(b *bufio.Reader, reader TcpReader, options *TrafficFilteringOptions) error
|
||||||
Analyze(item *OutputChannelItem, resolvedSource string, resolvedDestination string, namespace string) *Entry
|
Analyze(item *OutputChannelItem, resolvedSource string, resolvedDestination string, namespace string) *Entry
|
||||||
@@ -161,26 +150,21 @@ func (e *Emitting) Emit(item *OutputChannelItem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Entry struct {
|
type Entry struct {
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
Protocol Protocol `json:"proto"`
|
ProtocolId string `json:"protocol"`
|
||||||
Capture Capture `json:"capture"`
|
Capture Capture `json:"capture"`
|
||||||
Source *TCP `json:"src"`
|
Source *TCP `json:"src"`
|
||||||
Destination *TCP `json:"dst"`
|
Destination *TCP `json:"dst"`
|
||||||
Namespace string `json:"namespace"`
|
Namespace string `json:"namespace"`
|
||||||
Outgoing bool `json:"outgoing"`
|
Outgoing bool `json:"outgoing"`
|
||||||
Timestamp int64 `json:"timestamp"`
|
Timestamp int64 `json:"timestamp"`
|
||||||
StartTime time.Time `json:"startTime"`
|
StartTime time.Time `json:"startTime"`
|
||||||
Request map[string]interface{} `json:"request"`
|
Request map[string]interface{} `json:"request"`
|
||||||
Response map[string]interface{} `json:"response"`
|
Response map[string]interface{} `json:"response"`
|
||||||
RequestSize int `json:"requestSize"`
|
RequestSize int `json:"requestSize"`
|
||||||
ResponseSize int `json:"responseSize"`
|
ResponseSize int `json:"responseSize"`
|
||||||
ElapsedTime int64 `json:"elapsedTime"`
|
ElapsedTime int64 `json:"elapsedTime"`
|
||||||
Rules ApplicableRules `json:"rules,omitempty"`
|
Rules ApplicableRules `json:"rules,omitempty"`
|
||||||
ContractStatus ContractStatus `json:"contractStatus,omitempty"`
|
|
||||||
ContractRequestReason string `json:"contractRequestReason,omitempty"`
|
|
||||||
ContractResponseReason string `json:"contractResponseReason,omitempty"`
|
|
||||||
ContractContent string `json:"contractContent,omitempty"`
|
|
||||||
HTTPPair string `json:"httpPair,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type EntryWrapper struct {
|
type EntryWrapper struct {
|
||||||
@@ -193,22 +177,21 @@ type EntryWrapper struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type BaseEntry struct {
|
type BaseEntry struct {
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
Protocol Protocol `json:"proto,omitempty"`
|
Protocol Protocol `json:"proto,omitempty"`
|
||||||
Capture Capture `json:"capture"`
|
Capture Capture `json:"capture"`
|
||||||
Summary string `json:"summary,omitempty"`
|
Summary string `json:"summary,omitempty"`
|
||||||
SummaryQuery string `json:"summaryQuery,omitempty"`
|
SummaryQuery string `json:"summaryQuery,omitempty"`
|
||||||
Status int `json:"status"`
|
Status int `json:"status"`
|
||||||
StatusQuery string `json:"statusQuery"`
|
StatusQuery string `json:"statusQuery"`
|
||||||
Method string `json:"method,omitempty"`
|
Method string `json:"method,omitempty"`
|
||||||
MethodQuery string `json:"methodQuery,omitempty"`
|
MethodQuery string `json:"methodQuery,omitempty"`
|
||||||
Timestamp int64 `json:"timestamp,omitempty"`
|
Timestamp int64 `json:"timestamp,omitempty"`
|
||||||
Source *TCP `json:"src"`
|
Source *TCP `json:"src"`
|
||||||
Destination *TCP `json:"dst"`
|
Destination *TCP `json:"dst"`
|
||||||
IsOutgoing bool `json:"isOutgoing,omitempty"`
|
IsOutgoing bool `json:"isOutgoing,omitempty"`
|
||||||
Latency int64 `json:"latency"`
|
Latency int64 `json:"latency"`
|
||||||
Rules ApplicableRules `json:"rules,omitempty"`
|
Rules ApplicableRules `json:"rules,omitempty"`
|
||||||
ContractStatus ContractStatus `json:"contractStatus"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ApplicableRules struct {
|
type ApplicableRules struct {
|
||||||
@@ -217,15 +200,6 @@ type ApplicableRules struct {
|
|||||||
NumberOfRules int `json:"numberOfRules,omitempty"`
|
NumberOfRules int `json:"numberOfRules,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContractStatus int
|
|
||||||
|
|
||||||
type Contract struct {
|
|
||||||
Status ContractStatus `json:"status"`
|
|
||||||
RequestReason string `json:"requestReason"`
|
|
||||||
ResponseReason string `json:"responseReason"`
|
|
||||||
Content string `json:"content"`
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TABLE string = "table"
|
TABLE string = "table"
|
||||||
BODY string = "body"
|
BODY string = "body"
|
||||||
@@ -246,170 +220,6 @@ type TableData struct {
|
|||||||
Selector string `json:"selector"`
|
Selector string `json:"selector"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
|
||||||
TypeHttpRequest = iota
|
|
||||||
TypeHttpResponse
|
|
||||||
)
|
|
||||||
|
|
||||||
type HTTPPayload struct {
|
|
||||||
Type uint8
|
|
||||||
Data interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type HTTPPayloader interface {
|
|
||||||
MarshalJSON() ([]byte, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type HTTPWrapper struct {
|
|
||||||
Method string `json:"method"`
|
|
||||||
Url string `json:"url"`
|
|
||||||
Details interface{} `json:"details"`
|
|
||||||
RawRequest *HTTPRequestWrapper `json:"rawRequest"`
|
|
||||||
RawResponse *HTTPResponseWrapper `json:"rawResponse"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h HTTPPayload) MarshalJSON() ([]byte, error) {
|
|
||||||
_, testEnvEnabled := os.LookupEnv(mizuTestEnvVar)
|
|
||||||
switch h.Type {
|
|
||||||
case TypeHttpRequest:
|
|
||||||
harRequest, err := har.NewRequest(h.Data.(*http.Request), true)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("Failed converting request to HAR")
|
|
||||||
}
|
|
||||||
sort.Slice(harRequest.Headers, func(i, j int) bool {
|
|
||||||
if harRequest.Headers[i].Name < harRequest.Headers[j].Name {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if harRequest.Headers[i].Name > harRequest.Headers[j].Name {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return harRequest.Headers[i].Value < harRequest.Headers[j].Value
|
|
||||||
})
|
|
||||||
sort.Slice(harRequest.QueryString, func(i, j int) bool {
|
|
||||||
if harRequest.QueryString[i].Name < harRequest.QueryString[j].Name {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if harRequest.QueryString[i].Name > harRequest.QueryString[j].Name {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return harRequest.QueryString[i].Value < harRequest.QueryString[j].Value
|
|
||||||
})
|
|
||||||
if harRequest.PostData != nil {
|
|
||||||
sort.Slice(harRequest.PostData.Params, func(i, j int) bool {
|
|
||||||
if harRequest.PostData.Params[i].Name < harRequest.PostData.Params[j].Name {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if harRequest.PostData.Params[i].Name > harRequest.PostData.Params[j].Name {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return harRequest.PostData.Params[i].Value < harRequest.PostData.Params[j].Value
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if testEnvEnabled {
|
|
||||||
harRequest.URL = ""
|
|
||||||
}
|
|
||||||
var reqWrapper *HTTPRequestWrapper
|
|
||||||
if !testEnvEnabled {
|
|
||||||
reqWrapper = &HTTPRequestWrapper{Request: h.Data.(*http.Request)}
|
|
||||||
}
|
|
||||||
return json.Marshal(&HTTPWrapper{
|
|
||||||
Method: harRequest.Method,
|
|
||||||
Details: harRequest,
|
|
||||||
RawRequest: reqWrapper,
|
|
||||||
})
|
|
||||||
case TypeHttpResponse:
|
|
||||||
harResponse, err := har.NewResponse(h.Data.(*http.Response), true)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("Failed converting response to HAR")
|
|
||||||
}
|
|
||||||
sort.Slice(harResponse.Headers, func(i, j int) bool {
|
|
||||||
if harResponse.Headers[i].Name < harResponse.Headers[j].Name {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if harResponse.Headers[i].Name > harResponse.Headers[j].Name {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return harResponse.Headers[i].Value < harResponse.Headers[j].Value
|
|
||||||
})
|
|
||||||
sort.Slice(harResponse.Cookies, func(i, j int) bool {
|
|
||||||
if harResponse.Cookies[i].Name < harResponse.Cookies[j].Name {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if harResponse.Cookies[i].Name > harResponse.Cookies[j].Name {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return harResponse.Cookies[i].Value < harResponse.Cookies[j].Value
|
|
||||||
})
|
|
||||||
var resWrapper *HTTPResponseWrapper
|
|
||||||
if !testEnvEnabled {
|
|
||||||
resWrapper = &HTTPResponseWrapper{Response: h.Data.(*http.Response)}
|
|
||||||
}
|
|
||||||
return json.Marshal(&HTTPWrapper{
|
|
||||||
Method: "",
|
|
||||||
Url: "",
|
|
||||||
Details: harResponse,
|
|
||||||
RawResponse: resWrapper,
|
|
||||||
})
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("HTTP payload cannot be marshaled: %v", h.Type))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type HTTPWrapperTricky struct {
|
|
||||||
Method string `json:"method"`
|
|
||||||
Url string `json:"url"`
|
|
||||||
Details interface{} `json:"details"`
|
|
||||||
RawRequest *http.Request `json:"rawRequest"`
|
|
||||||
RawResponse *http.Response `json:"rawResponse"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type HTTPMessage struct {
|
|
||||||
IsRequest bool `json:"isRequest"`
|
|
||||||
CaptureTime time.Time `json:"captureTime"`
|
|
||||||
Payload HTTPWrapperTricky `json:"payload"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type HTTPRequestResponsePair struct {
|
|
||||||
Request HTTPMessage `json:"request"`
|
|
||||||
Response HTTPMessage `json:"response"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type HTTPRequestWrapper struct {
|
|
||||||
*http.Request
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *HTTPRequestWrapper) MarshalJSON() ([]byte, error) {
|
|
||||||
body, _ := ioutil.ReadAll(r.Request.Body)
|
|
||||||
r.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body))
|
|
||||||
return json.Marshal(&struct { //nolint
|
|
||||||
Body string `json:"Body,omitempty"`
|
|
||||||
GetBody string `json:"GetBody,omitempty"`
|
|
||||||
Cancel string `json:"Cancel,omitempty"`
|
|
||||||
*http.Request
|
|
||||||
}{
|
|
||||||
Body: string(body),
|
|
||||||
Request: r.Request,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type HTTPResponseWrapper struct {
|
|
||||||
*http.Response
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *HTTPResponseWrapper) MarshalJSON() ([]byte, error) {
|
|
||||||
body, _ := ioutil.ReadAll(r.Response.Body)
|
|
||||||
r.Response.Body = ioutil.NopCloser(bytes.NewBuffer(body))
|
|
||||||
return json.Marshal(&struct { //nolint
|
|
||||||
Body string `json:"Body,omitempty"`
|
|
||||||
GetBody string `json:"GetBody,omitempty"`
|
|
||||||
Cancel string `json:"Cancel,omitempty"`
|
|
||||||
*http.Response
|
|
||||||
}{
|
|
||||||
Body: string(body),
|
|
||||||
Response: r.Response,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type TcpReaderDataMsg interface {
|
type TcpReaderDataMsg interface {
|
||||||
GetBytes() []byte
|
GetBytes() []byte
|
||||||
GetTimestamp() time.Time
|
GetTimestamp() time.Time
|
||||||
|
|||||||
@@ -2,9 +2,6 @@ module github.com/up9inc/mizu/tap/api
|
|||||||
|
|
||||||
go 1.17
|
go 1.17
|
||||||
|
|
||||||
require (
|
require github.com/up9inc/mizu/tap/dbgctl v0.0.0
|
||||||
github.com/google/martian v2.1.0+incompatible
|
|
||||||
github.com/up9inc/mizu/tap/dbgctl v0.0.0
|
|
||||||
)
|
|
||||||
|
|
||||||
replace github.com/up9inc/mizu/tap/dbgctl v0.0.0 => ../dbgctl
|
replace github.com/up9inc/mizu/tap/dbgctl v0.0.0 => ../dbgctl
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
|
|
||||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
|
||||||
|
|||||||
@@ -13,4 +13,4 @@ test-pull-bin:
|
|||||||
|
|
||||||
test-pull-expect:
|
test-pull-expect:
|
||||||
@mkdir -p expect
|
@mkdir -p expect
|
||||||
@[ "${skipexpect}" ] && echo "Skipping downloading expected JSONs" || gsutil -o 'GSUtil:parallel_process_count=5' -o 'GSUtil:parallel_thread_count=5' -m cp -r gs://static.up9.io/mizu/test-pcap/expect8/amqp/\* expect
|
@[ "${skipexpect}" ] && echo "Skipping downloading expected JSONs" || gsutil -o 'GSUtil:parallel_process_count=5' -o 'GSUtil:parallel_thread_count=5' -m cp -r gs://static.up9.io/mizu/test-pcap/expect13/amqp/\* expect
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.0 // indirect
|
github.com/davecgh/go-spew v1.1.0 // indirect
|
||||||
github.com/google/martian v2.1.0+incompatible // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/up9inc/mizu/tap/dbgctl v0.0.0 // indirect
|
github.com/up9inc/mizu/tap/dbgctl v0.0.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
|
|
||||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
|||||||
@@ -26,12 +26,20 @@ var protocol = api.Protocol{
|
|||||||
Priority: 1,
|
Priority: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var protocolsMap = map[string]*api.Protocol{
|
||||||
|
fmt.Sprintf("%s/%s/%s", protocol.Name, protocol.Version, protocol.Abbreviation): &protocol,
|
||||||
|
}
|
||||||
|
|
||||||
type dissecting string
|
type dissecting string
|
||||||
|
|
||||||
func (d dissecting) Register(extension *api.Extension) {
|
func (d dissecting) Register(extension *api.Extension) {
|
||||||
extension.Protocol = &protocol
|
extension.Protocol = &protocol
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d dissecting) GetProtocols() map[string]*api.Protocol {
|
||||||
|
return protocolsMap
|
||||||
|
}
|
||||||
|
|
||||||
func (d dissecting) Ping() {
|
func (d dissecting) Ping() {
|
||||||
log.Printf("pong %s", protocol.Name)
|
log.Printf("pong %s", protocol.Name)
|
||||||
}
|
}
|
||||||
@@ -214,8 +222,8 @@ func (d dissecting) Analyze(item *api.OutputChannelItem, resolvedSource string,
|
|||||||
|
|
||||||
reqDetails["method"] = request["method"]
|
reqDetails["method"] = request["method"]
|
||||||
return &api.Entry{
|
return &api.Entry{
|
||||||
Protocol: protocol,
|
ProtocolId: fmt.Sprintf("%s/%s/%s", protocol.Name, protocol.Version, protocol.Abbreviation),
|
||||||
Capture: item.Capture,
|
Capture: item.Capture,
|
||||||
Source: &api.TCP{
|
Source: &api.TCP{
|
||||||
Name: resolvedSource,
|
Name: resolvedSource,
|
||||||
IP: item.ConnectionInfo.ClientIP,
|
IP: item.ConnectionInfo.ClientIP,
|
||||||
@@ -276,22 +284,21 @@ func (d dissecting) Summarize(entry *api.Entry) *api.BaseEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &api.BaseEntry{
|
return &api.BaseEntry{
|
||||||
Id: entry.Id,
|
Id: entry.Id,
|
||||||
Protocol: entry.Protocol,
|
Protocol: *protocolsMap[entry.ProtocolId],
|
||||||
Capture: entry.Capture,
|
Capture: entry.Capture,
|
||||||
Summary: summary,
|
Summary: summary,
|
||||||
SummaryQuery: summaryQuery,
|
SummaryQuery: summaryQuery,
|
||||||
Status: 0,
|
Status: 0,
|
||||||
StatusQuery: "",
|
StatusQuery: "",
|
||||||
Method: method,
|
Method: method,
|
||||||
MethodQuery: methodQuery,
|
MethodQuery: methodQuery,
|
||||||
Timestamp: entry.Timestamp,
|
Timestamp: entry.Timestamp,
|
||||||
Source: entry.Source,
|
Source: entry.Source,
|
||||||
Destination: entry.Destination,
|
Destination: entry.Destination,
|
||||||
IsOutgoing: entry.Outgoing,
|
IsOutgoing: entry.Outgoing,
|
||||||
Latency: entry.ElapsedTime,
|
Latency: entry.ElapsedTime,
|
||||||
Rules: entry.Rules,
|
Rules: entry.Rules,
|
||||||
ContractStatus: entry.ContractStatus,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,7 +330,7 @@ func (d dissecting) Represent(request map[string]interface{}, response map[strin
|
|||||||
|
|
||||||
func (d dissecting) Macros() map[string]string {
|
func (d dissecting) Macros() map[string]string {
|
||||||
return map[string]string{
|
return map[string]string{
|
||||||
`amqp`: fmt.Sprintf(`proto.name == "%s"`, protocol.Name),
|
`amqp`: fmt.Sprintf(`protocol == "%s/%s/%s"`, protocol.Name, protocol.Version, protocol.Abbreviation),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func TestRegister(t *testing.T) {
|
|||||||
|
|
||||||
func TestMacros(t *testing.T) {
|
func TestMacros(t *testing.T) {
|
||||||
expectedMacros := map[string]string{
|
expectedMacros := map[string]string{
|
||||||
"amqp": `proto.name == "amqp"`,
|
"amqp": `protocol == "amqp/0-9-1/AMQP"`,
|
||||||
}
|
}
|
||||||
dissector := NewDissector()
|
dissector := NewDissector()
|
||||||
macros := dissector.Macros()
|
macros := dissector.Macros()
|
||||||
|
|||||||
@@ -13,4 +13,4 @@ test-pull-bin:
|
|||||||
|
|
||||||
test-pull-expect:
|
test-pull-expect:
|
||||||
@mkdir -p expect
|
@mkdir -p expect
|
||||||
@[ "${skipexpect}" ] && echo "Skipping downloading expected JSONs" || gsutil -o 'GSUtil:parallel_process_count=5' -o 'GSUtil:parallel_thread_count=5' -m cp -r gs://static.up9.io/mizu/test-pcap/expect10/http/\* expect
|
@[ "${skipexpect}" ] && echo "Skipping downloading expected JSONs" || gsutil -o 'GSUtil:parallel_process_count=5' -o 'GSUtil:parallel_thread_count=5' -m cp -r gs://static.up9.io/mizu/test-pcap/expect13/http/\* expect
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ go 1.17
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/beevik/etree v1.1.0
|
github.com/beevik/etree v1.1.0
|
||||||
|
github.com/google/martian v2.1.0+incompatible
|
||||||
github.com/mertyildiran/gqlparser/v2 v2.4.6
|
github.com/mertyildiran/gqlparser/v2 v2.4.6
|
||||||
github.com/stretchr/testify v1.7.0
|
github.com/stretchr/testify v1.7.0
|
||||||
github.com/up9inc/mizu/tap/api v0.0.0
|
github.com/up9inc/mizu/tap/api v0.0.0
|
||||||
@@ -12,7 +13,6 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/google/martian v2.1.0+incompatible // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/up9inc/mizu/tap/dbgctl v0.0.0 // indirect
|
github.com/up9inc/mizu/tap/dbgctl v0.0.0 // indirect
|
||||||
golang.org/x/text v0.3.7 // indirect
|
golang.org/x/text v0.3.7 // indirect
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ func replaceForwardedFor(item *api.OutputChannelItem) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
request := item.Pair.Request.Payload.(api.HTTPPayload).Data.(*http.Request)
|
request := item.Pair.Request.Payload.(HTTPPayload).Data.(*http.Request)
|
||||||
|
|
||||||
forwardedFor := request.Header.Get("X-Forwarded-For")
|
forwardedFor := request.Header.Get("X-Forwarded-For")
|
||||||
if forwardedFor == "" {
|
if forwardedFor == "" {
|
||||||
|
|||||||
@@ -98,6 +98,15 @@ var graphQL2Protocol = api.Protocol{
|
|||||||
Priority: 0,
|
Priority: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var protocolsMap = map[string]*api.Protocol{
|
||||||
|
fmt.Sprintf("%s/%s/%s", http10protocol.Name, http10protocol.Version, http10protocol.Abbreviation): &http10protocol,
|
||||||
|
fmt.Sprintf("%s/%s/%s", http11protocol.Name, http11protocol.Version, http11protocol.Abbreviation): &http11protocol,
|
||||||
|
fmt.Sprintf("%s/%s/%s", http2Protocol.Name, http2Protocol.Version, http2Protocol.Abbreviation): &http2Protocol,
|
||||||
|
fmt.Sprintf("%s/%s/%s", grpcProtocol.Name, grpcProtocol.Version, grpcProtocol.Abbreviation): &grpcProtocol,
|
||||||
|
fmt.Sprintf("%s/%s/%s", graphQL1Protocol.Name, graphQL1Protocol.Version, graphQL1Protocol.Abbreviation): &graphQL1Protocol,
|
||||||
|
fmt.Sprintf("%s/%s/%s", graphQL2Protocol.Name, graphQL2Protocol.Version, graphQL2Protocol.Abbreviation): &graphQL2Protocol,
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TypeHttpRequest = iota
|
TypeHttpRequest = iota
|
||||||
TypeHttpResponse
|
TypeHttpResponse
|
||||||
@@ -109,6 +118,10 @@ func (d dissecting) Register(extension *api.Extension) {
|
|||||||
extension.Protocol = &http11protocol
|
extension.Protocol = &http11protocol
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d dissecting) GetProtocols() map[string]*api.Protocol {
|
||||||
|
return protocolsMap
|
||||||
|
}
|
||||||
|
|
||||||
func (d dissecting) Ping() {
|
func (d dissecting) Ping() {
|
||||||
log.Printf("pong %s", http11protocol.Name)
|
log.Printf("pong %s", http11protocol.Name)
|
||||||
}
|
}
|
||||||
@@ -279,10 +292,10 @@ func (d dissecting) Analyze(item *api.OutputChannelItem, resolvedSource string,
|
|||||||
if elapsedTime < 0 {
|
if elapsedTime < 0 {
|
||||||
elapsedTime = 0
|
elapsedTime = 0
|
||||||
}
|
}
|
||||||
httpPair, _ := json.Marshal(item.Pair)
|
|
||||||
return &api.Entry{
|
return &api.Entry{
|
||||||
Protocol: item.Protocol,
|
ProtocolId: fmt.Sprintf("%s/%s/%s", item.Protocol.Name, item.Protocol.Version, item.Protocol.Abbreviation),
|
||||||
Capture: item.Capture,
|
Capture: item.Capture,
|
||||||
Source: &api.TCP{
|
Source: &api.TCP{
|
||||||
Name: resolvedSource,
|
Name: resolvedSource,
|
||||||
IP: item.ConnectionInfo.ClientIP,
|
IP: item.ConnectionInfo.ClientIP,
|
||||||
@@ -302,7 +315,6 @@ func (d dissecting) Analyze(item *api.OutputChannelItem, resolvedSource string,
|
|||||||
Timestamp: item.Timestamp,
|
Timestamp: item.Timestamp,
|
||||||
StartTime: item.Pair.Request.CaptureTime,
|
StartTime: item.Pair.Request.CaptureTime,
|
||||||
ElapsedTime: elapsedTime,
|
ElapsedTime: elapsedTime,
|
||||||
HTTPPair: string(httpPair),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,22 +327,21 @@ func (d dissecting) Summarize(entry *api.Entry) *api.BaseEntry {
|
|||||||
statusQuery := fmt.Sprintf(`response.status == %d`, status)
|
statusQuery := fmt.Sprintf(`response.status == %d`, status)
|
||||||
|
|
||||||
return &api.BaseEntry{
|
return &api.BaseEntry{
|
||||||
Id: entry.Id,
|
Id: entry.Id,
|
||||||
Protocol: entry.Protocol,
|
Protocol: *protocolsMap[entry.ProtocolId],
|
||||||
Capture: entry.Capture,
|
Capture: entry.Capture,
|
||||||
Summary: summary,
|
Summary: summary,
|
||||||
SummaryQuery: summaryQuery,
|
SummaryQuery: summaryQuery,
|
||||||
Status: status,
|
Status: status,
|
||||||
StatusQuery: statusQuery,
|
StatusQuery: statusQuery,
|
||||||
Method: method,
|
Method: method,
|
||||||
MethodQuery: methodQuery,
|
MethodQuery: methodQuery,
|
||||||
Timestamp: entry.Timestamp,
|
Timestamp: entry.Timestamp,
|
||||||
Source: entry.Source,
|
Source: entry.Source,
|
||||||
Destination: entry.Destination,
|
Destination: entry.Destination,
|
||||||
IsOutgoing: entry.Outgoing,
|
IsOutgoing: entry.Outgoing,
|
||||||
Latency: entry.ElapsedTime,
|
Latency: entry.ElapsedTime,
|
||||||
Rules: entry.Rules,
|
Rules: entry.Rules,
|
||||||
ContractStatus: entry.ContractStatus,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -505,10 +516,10 @@ func (d dissecting) Represent(request map[string]interface{}, response map[strin
|
|||||||
|
|
||||||
func (d dissecting) Macros() map[string]string {
|
func (d dissecting) Macros() map[string]string {
|
||||||
return map[string]string{
|
return map[string]string{
|
||||||
`http`: fmt.Sprintf(`proto.name == "%s" and proto.version.startsWith("%c")`, http11protocol.Name, http11protocol.Version[0]),
|
`http`: fmt.Sprintf(`protocol == "%s/%s/%s" or protocol == "%s/%s/%s"`, http10protocol.Name, http10protocol.Version, http10protocol.Abbreviation, http11protocol.Name, http11protocol.Version, http11protocol.Abbreviation),
|
||||||
`http2`: fmt.Sprintf(`proto.name == "%s" and proto.version == "%s"`, http11protocol.Name, http2Protocol.Version),
|
`http2`: fmt.Sprintf(`protocol == "%s/%s/%s"`, http2Protocol.Name, http2Protocol.Version, http2Protocol.Abbreviation),
|
||||||
`grpc`: fmt.Sprintf(`proto.name == "%s" and proto.version == "%s" and proto.macro == "%s"`, http11protocol.Name, grpcProtocol.Version, grpcProtocol.Macro),
|
`grpc`: fmt.Sprintf(`protocol == "%s/%s/%s"`, grpcProtocol.Name, grpcProtocol.Version, grpcProtocol.Abbreviation),
|
||||||
`gql`: fmt.Sprintf(`proto.name == "%s" and proto.macro == "%s"`, graphQL1Protocol.Name, graphQL1Protocol.Macro),
|
`gql`: fmt.Sprintf(`protocol == "%s/%s/%s" or protocol == "%s/%s/%s"`, graphQL1Protocol.Name, graphQL1Protocol.Version, graphQL1Protocol.Abbreviation, graphQL2Protocol.Name, graphQL2Protocol.Version, graphQL2Protocol.Abbreviation),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,10 +44,10 @@ func TestRegister(t *testing.T) {
|
|||||||
|
|
||||||
func TestMacros(t *testing.T) {
|
func TestMacros(t *testing.T) {
|
||||||
expectedMacros := map[string]string{
|
expectedMacros := map[string]string{
|
||||||
"http": `proto.name == "http" and proto.version.startsWith("1")`,
|
"http": `protocol == "http/1.0/HTTP" or protocol == "http/1.1/HTTP"`,
|
||||||
"http2": `proto.name == "http" and proto.version == "2.0"`,
|
"http2": `protocol == "http/2.0/HTTP/2"`,
|
||||||
"grpc": `proto.name == "http" and proto.version == "2.0" and proto.macro == "grpc"`,
|
"grpc": `protocol == "http/2.0/gRPC"`,
|
||||||
"gql": `proto.name == "http" and proto.macro == "gql"`,
|
"gql": `protocol == "http/1.1/GQL" or protocol == "http/2.0/GQL"`,
|
||||||
}
|
}
|
||||||
dissector := NewDissector()
|
dissector := NewDissector()
|
||||||
macros := dissector.Macros()
|
macros := dissector.Macros()
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ func (matcher *requestResponseMatcher) registerRequest(ident string, request *ht
|
|||||||
IsRequest: true,
|
IsRequest: true,
|
||||||
CaptureTime: captureTime,
|
CaptureTime: captureTime,
|
||||||
CaptureSize: captureSize,
|
CaptureSize: captureSize,
|
||||||
Payload: api.HTTPPayload{
|
Payload: HTTPPayload{
|
||||||
Type: TypeHttpRequest,
|
Type: TypeHttpRequest,
|
||||||
Data: request,
|
Data: request,
|
||||||
},
|
},
|
||||||
@@ -53,7 +53,7 @@ func (matcher *requestResponseMatcher) registerResponse(ident string, response *
|
|||||||
IsRequest: false,
|
IsRequest: false,
|
||||||
CaptureTime: captureTime,
|
CaptureTime: captureTime,
|
||||||
CaptureSize: captureSize,
|
CaptureSize: captureSize,
|
||||||
Payload: api.HTTPPayload{
|
Payload: HTTPPayload{
|
||||||
Type: TypeHttpResponse,
|
Type: TypeHttpResponse,
|
||||||
Data: response,
|
Data: response,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ func IsIgnoredUserAgent(item *api.OutputChannelItem, options *api.TrafficFilteri
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
request := item.Pair.Request.Payload.(api.HTTPPayload).Data.(*http.Request)
|
request := item.Pair.Request.Payload.(HTTPPayload).Data.(*http.Request)
|
||||||
|
|
||||||
for headerKey, headerValues := range request.Header {
|
for headerKey, headerValues := range request.Header {
|
||||||
if strings.ToLower(headerKey) == userAgent {
|
if strings.ToLower(headerKey) == userAgent {
|
||||||
@@ -50,8 +50,8 @@ func IsIgnoredUserAgent(item *api.OutputChannelItem, options *api.TrafficFilteri
|
|||||||
}
|
}
|
||||||
|
|
||||||
func FilterSensitiveData(item *api.OutputChannelItem, options *api.TrafficFilteringOptions) {
|
func FilterSensitiveData(item *api.OutputChannelItem, options *api.TrafficFilteringOptions) {
|
||||||
request := item.Pair.Request.Payload.(api.HTTPPayload).Data.(*http.Request)
|
request := item.Pair.Request.Payload.(HTTPPayload).Data.(*http.Request)
|
||||||
response := item.Pair.Response.Payload.(api.HTTPPayload).Data.(*http.Response)
|
response := item.Pair.Response.Payload.(HTTPPayload).Data.(*http.Response)
|
||||||
|
|
||||||
filterHeaders(&request.Header)
|
filterHeaders(&request.Header)
|
||||||
filterHeaders(&response.Header)
|
filterHeaders(&response.Header)
|
||||||
|
|||||||
100
tap/extensions/http/structs.go
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/google/martian/har"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HTTPPayload struct {
|
||||||
|
Type uint8
|
||||||
|
Data interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type HTTPPayloader interface {
|
||||||
|
MarshalJSON() ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type HTTPWrapper struct {
|
||||||
|
Method string `json:"method"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
Details interface{} `json:"details"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h HTTPPayload) MarshalJSON() ([]byte, error) {
|
||||||
|
switch h.Type {
|
||||||
|
case TypeHttpRequest:
|
||||||
|
harRequest, err := har.NewRequest(h.Data.(*http.Request), true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("Failed converting request to HAR")
|
||||||
|
}
|
||||||
|
sort.Slice(harRequest.Headers, func(i, j int) bool {
|
||||||
|
if harRequest.Headers[i].Name < harRequest.Headers[j].Name {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if harRequest.Headers[i].Name > harRequest.Headers[j].Name {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return harRequest.Headers[i].Value < harRequest.Headers[j].Value
|
||||||
|
})
|
||||||
|
sort.Slice(harRequest.QueryString, func(i, j int) bool {
|
||||||
|
if harRequest.QueryString[i].Name < harRequest.QueryString[j].Name {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if harRequest.QueryString[i].Name > harRequest.QueryString[j].Name {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return harRequest.QueryString[i].Value < harRequest.QueryString[j].Value
|
||||||
|
})
|
||||||
|
if harRequest.PostData != nil {
|
||||||
|
sort.Slice(harRequest.PostData.Params, func(i, j int) bool {
|
||||||
|
if harRequest.PostData.Params[i].Name < harRequest.PostData.Params[j].Name {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if harRequest.PostData.Params[i].Name > harRequest.PostData.Params[j].Name {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return harRequest.PostData.Params[i].Value < harRequest.PostData.Params[j].Value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return json.Marshal(&HTTPWrapper{
|
||||||
|
Method: harRequest.Method,
|
||||||
|
Url: "",
|
||||||
|
Details: harRequest,
|
||||||
|
})
|
||||||
|
case TypeHttpResponse:
|
||||||
|
harResponse, err := har.NewResponse(h.Data.(*http.Response), true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("Failed converting response to HAR")
|
||||||
|
}
|
||||||
|
sort.Slice(harResponse.Headers, func(i, j int) bool {
|
||||||
|
if harResponse.Headers[i].Name < harResponse.Headers[j].Name {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if harResponse.Headers[i].Name > harResponse.Headers[j].Name {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return harResponse.Headers[i].Value < harResponse.Headers[j].Value
|
||||||
|
})
|
||||||
|
sort.Slice(harResponse.Cookies, func(i, j int) bool {
|
||||||
|
if harResponse.Cookies[i].Name < harResponse.Cookies[j].Name {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if harResponse.Cookies[i].Name > harResponse.Cookies[j].Name {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return harResponse.Cookies[i].Value < harResponse.Cookies[j].Value
|
||||||
|
})
|
||||||
|
return json.Marshal(&HTTPWrapper{
|
||||||
|
Method: "",
|
||||||
|
Url: "",
|
||||||
|
Details: harResponse,
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("HTTP payload cannot be marshaled: %v", h.Type))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,4 +13,4 @@ test-pull-bin:
|
|||||||
|
|
||||||
test-pull-expect:
|
test-pull-expect:
|
||||||
@mkdir -p expect
|
@mkdir -p expect
|
||||||
@[ "${skipexpect}" ] && echo "Skipping downloading expected JSONs" || gsutil -o 'GSUtil:parallel_process_count=5' -o 'GSUtil:parallel_thread_count=5' -m cp -r gs://static.up9.io/mizu/test-pcap/expect9/kafka/\* expect
|
@[ "${skipexpect}" ] && echo "Skipping downloading expected JSONs" || gsutil -o 'GSUtil:parallel_process_count=5' -o 'GSUtil:parallel_thread_count=5' -m cp -r gs://static.up9.io/mizu/test-pcap/expect13/kafka/\* expect
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ require (
|
|||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.0 // indirect
|
github.com/davecgh/go-spew v1.1.0 // indirect
|
||||||
github.com/golang/snappy v0.0.4 // indirect
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
github.com/google/martian v2.1.0+incompatible // indirect
|
|
||||||
github.com/klauspost/compress v1.14.2 // indirect
|
github.com/klauspost/compress v1.14.2 // indirect
|
||||||
github.com/pierrec/lz4 v2.6.1+incompatible // indirect
|
github.com/pierrec/lz4 v2.6.1+incompatible // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
|||||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
|
github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
|
||||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
|
|
||||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
|
||||||
github.com/klauspost/compress v1.9.8/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
github.com/klauspost/compress v1.9.8/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||||
github.com/klauspost/compress v1.14.2 h1:S0OHlFk/Gbon/yauFJ4FfJJF5V0fc5HbBTJazi28pRw=
|
github.com/klauspost/compress v1.14.2 h1:S0OHlFk/Gbon/yauFJ4FfJJF5V0fc5HbBTJazi28pRw=
|
||||||
github.com/klauspost/compress v1.14.2/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
github.com/klauspost/compress v1.14.2/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||||
|
|||||||
@@ -24,12 +24,20 @@ var _protocol = api.Protocol{
|
|||||||
Priority: 2,
|
Priority: 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var protocolsMap = map[string]*api.Protocol{
|
||||||
|
fmt.Sprintf("%s/%s/%s", _protocol.Name, _protocol.Version, _protocol.Abbreviation): &_protocol,
|
||||||
|
}
|
||||||
|
|
||||||
type dissecting string
|
type dissecting string
|
||||||
|
|
||||||
func (d dissecting) Register(extension *api.Extension) {
|
func (d dissecting) Register(extension *api.Extension) {
|
||||||
extension.Protocol = &_protocol
|
extension.Protocol = &_protocol
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d dissecting) GetProtocols() map[string]*api.Protocol {
|
||||||
|
return protocolsMap
|
||||||
|
}
|
||||||
|
|
||||||
func (d dissecting) Ping() {
|
func (d dissecting) Ping() {
|
||||||
log.Printf("pong %s", _protocol.Name)
|
log.Printf("pong %s", _protocol.Name)
|
||||||
}
|
}
|
||||||
@@ -62,8 +70,8 @@ func (d dissecting) Analyze(item *api.OutputChannelItem, resolvedSource string,
|
|||||||
elapsedTime = 0
|
elapsedTime = 0
|
||||||
}
|
}
|
||||||
return &api.Entry{
|
return &api.Entry{
|
||||||
Protocol: _protocol,
|
ProtocolId: fmt.Sprintf("%s/%s/%s", _protocol.Name, _protocol.Version, _protocol.Abbreviation),
|
||||||
Capture: item.Capture,
|
Capture: item.Capture,
|
||||||
Source: &api.TCP{
|
Source: &api.TCP{
|
||||||
Name: resolvedSource,
|
Name: resolvedSource,
|
||||||
IP: item.ConnectionInfo.ClientIP,
|
IP: item.ConnectionInfo.ClientIP,
|
||||||
@@ -186,22 +194,21 @@ func (d dissecting) Summarize(entry *api.Entry) *api.BaseEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &api.BaseEntry{
|
return &api.BaseEntry{
|
||||||
Id: entry.Id,
|
Id: entry.Id,
|
||||||
Protocol: entry.Protocol,
|
Protocol: *protocolsMap[entry.ProtocolId],
|
||||||
Capture: entry.Capture,
|
Capture: entry.Capture,
|
||||||
Summary: summary,
|
Summary: summary,
|
||||||
SummaryQuery: summaryQuery,
|
SummaryQuery: summaryQuery,
|
||||||
Status: status,
|
Status: status,
|
||||||
StatusQuery: statusQuery,
|
StatusQuery: statusQuery,
|
||||||
Method: method,
|
Method: method,
|
||||||
MethodQuery: methodQuery,
|
MethodQuery: methodQuery,
|
||||||
Timestamp: entry.Timestamp,
|
Timestamp: entry.Timestamp,
|
||||||
Source: entry.Source,
|
Source: entry.Source,
|
||||||
Destination: entry.Destination,
|
Destination: entry.Destination,
|
||||||
IsOutgoing: entry.Outgoing,
|
IsOutgoing: entry.Outgoing,
|
||||||
Latency: entry.ElapsedTime,
|
Latency: entry.ElapsedTime,
|
||||||
Rules: entry.Rules,
|
Rules: entry.Rules,
|
||||||
ContractStatus: entry.ContractStatus,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,7 +251,7 @@ func (d dissecting) Represent(request map[string]interface{}, response map[strin
|
|||||||
|
|
||||||
func (d dissecting) Macros() map[string]string {
|
func (d dissecting) Macros() map[string]string {
|
||||||
return map[string]string{
|
return map[string]string{
|
||||||
`kafka`: fmt.Sprintf(`proto.name == "%s"`, _protocol.Name),
|
`kafka`: fmt.Sprintf(`protocol == "%s/%s/%s"`, _protocol.Name, _protocol.Version, _protocol.Abbreviation),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func TestRegister(t *testing.T) {
|
|||||||
|
|
||||||
func TestMacros(t *testing.T) {
|
func TestMacros(t *testing.T) {
|
||||||
expectedMacros := map[string]string{
|
expectedMacros := map[string]string{
|
||||||
"kafka": `proto.name == "kafka"`,
|
"kafka": `protocol == "kafka/12/KAFKA"`,
|
||||||
}
|
}
|
||||||
dissector := NewDissector()
|
dissector := NewDissector()
|
||||||
macros := dissector.Macros()
|
macros := dissector.Macros()
|
||||||
|
|||||||
@@ -13,4 +13,4 @@ test-pull-bin:
|
|||||||
|
|
||||||
test-pull-expect:
|
test-pull-expect:
|
||||||
@mkdir -p expect
|
@mkdir -p expect
|
||||||
@[ "${skipexpect}" ] && echo "Skipping downloading expected JSONs" || gsutil -o 'GSUtil:parallel_process_count=5' -o 'GSUtil:parallel_thread_count=5' -m cp -r gs://static.up9.io/mizu/test-pcap/expect8/redis/\* expect
|
@[ "${skipexpect}" ] && echo "Skipping downloading expected JSONs" || gsutil -o 'GSUtil:parallel_process_count=5' -o 'GSUtil:parallel_thread_count=5' -m cp -r gs://static.up9.io/mizu/test-pcap/expect13/redis/\* expect
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.0 // indirect
|
github.com/davecgh/go-spew v1.1.0 // indirect
|
||||||
github.com/google/martian v2.1.0+incompatible // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/up9inc/mizu/tap/dbgctl v0.0.0 // indirect
|
github.com/up9inc/mizu/tap/dbgctl v0.0.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
|
|
||||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
|||||||
@@ -24,12 +24,20 @@ var protocol = api.Protocol{
|
|||||||
Priority: 3,
|
Priority: 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var protocolsMap = map[string]*api.Protocol{
|
||||||
|
fmt.Sprintf("%s/%s/%s", protocol.Name, protocol.Version, protocol.Abbreviation): &protocol,
|
||||||
|
}
|
||||||
|
|
||||||
type dissecting string
|
type dissecting string
|
||||||
|
|
||||||
func (d dissecting) Register(extension *api.Extension) {
|
func (d dissecting) Register(extension *api.Extension) {
|
||||||
extension.Protocol = &protocol
|
extension.Protocol = &protocol
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d dissecting) GetProtocols() map[string]*api.Protocol {
|
||||||
|
return protocolsMap
|
||||||
|
}
|
||||||
|
|
||||||
func (d dissecting) Ping() {
|
func (d dissecting) Ping() {
|
||||||
log.Printf("pong %s", protocol.Name)
|
log.Printf("pong %s", protocol.Name)
|
||||||
}
|
}
|
||||||
@@ -70,8 +78,8 @@ func (d dissecting) Analyze(item *api.OutputChannelItem, resolvedSource string,
|
|||||||
elapsedTime = 0
|
elapsedTime = 0
|
||||||
}
|
}
|
||||||
return &api.Entry{
|
return &api.Entry{
|
||||||
Protocol: protocol,
|
ProtocolId: fmt.Sprintf("%s/%s/%s", protocol.Name, protocol.Version, protocol.Abbreviation),
|
||||||
Capture: item.Capture,
|
Capture: item.Capture,
|
||||||
Source: &api.TCP{
|
Source: &api.TCP{
|
||||||
Name: resolvedSource,
|
Name: resolvedSource,
|
||||||
IP: item.ConnectionInfo.ClientIP,
|
IP: item.ConnectionInfo.ClientIP,
|
||||||
@@ -114,22 +122,21 @@ func (d dissecting) Summarize(entry *api.Entry) *api.BaseEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &api.BaseEntry{
|
return &api.BaseEntry{
|
||||||
Id: entry.Id,
|
Id: entry.Id,
|
||||||
Protocol: entry.Protocol,
|
Protocol: *protocolsMap[entry.ProtocolId],
|
||||||
Capture: entry.Capture,
|
Capture: entry.Capture,
|
||||||
Summary: summary,
|
Summary: summary,
|
||||||
SummaryQuery: summaryQuery,
|
SummaryQuery: summaryQuery,
|
||||||
Status: status,
|
Status: status,
|
||||||
StatusQuery: statusQuery,
|
StatusQuery: statusQuery,
|
||||||
Method: method,
|
Method: method,
|
||||||
MethodQuery: methodQuery,
|
MethodQuery: methodQuery,
|
||||||
Timestamp: entry.Timestamp,
|
Timestamp: entry.Timestamp,
|
||||||
Source: entry.Source,
|
Source: entry.Source,
|
||||||
Destination: entry.Destination,
|
Destination: entry.Destination,
|
||||||
IsOutgoing: entry.Outgoing,
|
IsOutgoing: entry.Outgoing,
|
||||||
Latency: entry.ElapsedTime,
|
Latency: entry.ElapsedTime,
|
||||||
Rules: entry.Rules,
|
Rules: entry.Rules,
|
||||||
ContractStatus: entry.ContractStatus,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +152,7 @@ func (d dissecting) Represent(request map[string]interface{}, response map[strin
|
|||||||
|
|
||||||
func (d dissecting) Macros() map[string]string {
|
func (d dissecting) Macros() map[string]string {
|
||||||
return map[string]string{
|
return map[string]string{
|
||||||
`redis`: fmt.Sprintf(`proto.name == "%s"`, protocol.Name),
|
`redis`: fmt.Sprintf(`protocol == "%s/%s/%s"`, protocol.Name, protocol.Version, protocol.Abbreviation),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ func TestRegister(t *testing.T) {
|
|||||||
|
|
||||||
func TestMacros(t *testing.T) {
|
func TestMacros(t *testing.T) {
|
||||||
expectedMacros := map[string]string{
|
expectedMacros := map[string]string{
|
||||||
"redis": `proto.name == "redis"`,
|
"redis": `protocol == "redis/3.x/REDIS"`,
|
||||||
}
|
}
|
||||||
dissector := NewDissector()
|
dissector := NewDissector()
|
||||||
macros := dissector.Macros()
|
macros := dissector.Macros()
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ require (
|
|||||||
github.com/go-errors/errors v1.4.2
|
github.com/go-errors/errors v1.4.2
|
||||||
github.com/google/gopacket v1.1.19
|
github.com/google/gopacket v1.1.19
|
||||||
github.com/hashicorp/golang-lru v0.5.4
|
github.com/hashicorp/golang-lru v0.5.4
|
||||||
github.com/knightsc/gapstone v0.0.0-20211014144438-5e0e64002a6e
|
github.com/knightsc/gapstone v0.0.0-20191231144527-6fa5afaf11a9
|
||||||
github.com/shirou/gopsutil v3.21.11+incompatible
|
github.com/shirou/gopsutil v3.21.11+incompatible
|
||||||
github.com/struCoder/pidusage v0.2.1
|
github.com/struCoder/pidusage v0.2.1
|
||||||
github.com/up9inc/mizu/logger v0.0.0
|
github.com/up9inc/mizu/logger v0.0.0
|
||||||
@@ -24,7 +24,6 @@ require (
|
|||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/google/go-cmp v0.5.7 // indirect
|
github.com/google/go-cmp v0.5.7 // indirect
|
||||||
github.com/google/gofuzz v1.2.0 // indirect
|
github.com/google/gofuzz v1.2.0 // indirect
|
||||||
github.com/google/martian v2.1.0+incompatible // indirect
|
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
|||||||
@@ -68,8 +68,6 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
|||||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||||
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||||
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
|
|
||||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
|
||||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU=
|
github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU=
|
||||||
github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA=
|
github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA=
|
||||||
@@ -83,8 +81,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
|
|||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
github.com/knightsc/gapstone v0.0.0-20211014144438-5e0e64002a6e h1:6J5obSn9umEThiYzWzndcPOZR0Qj/sVCZpH6V1G7yNE=
|
github.com/knightsc/gapstone v0.0.0-20191231144527-6fa5afaf11a9 h1:1KszOoXSFt0aRQ6wxxcKm7QKgfLPI0TWO47UcY/f+vA=
|
||||||
github.com/knightsc/gapstone v0.0.0-20211014144438-5e0e64002a6e/go.mod h1:1K5hEzsMBLTPdRJKEHqBFJ8Zt2VRqDhomcQ11KH0WW4=
|
github.com/knightsc/gapstone v0.0.0-20191231144527-6fa5afaf11a9/go.mod h1:1K5hEzsMBLTPdRJKEHqBFJ8Zt2VRqDhomcQ11KH0WW4=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ func NewTcpAssembler(outputItems chan *api.OutputChannelItem, streamsMap api.Tcp
|
|||||||
|
|
||||||
maxBufferedPagesTotal := GetMaxBufferedPagesPerConnection()
|
maxBufferedPagesTotal := GetMaxBufferedPagesPerConnection()
|
||||||
maxBufferedPagesPerConnection := GetMaxBufferedPagesTotal()
|
maxBufferedPagesPerConnection := GetMaxBufferedPagesTotal()
|
||||||
logger.Log.Infof("Assembler options: maxBufferedPagesTotal=%d, maxBufferedPagesPerConnection=%d, opts=%v",
|
logger.Log.Infof("Assembler options: maxBufferedPagesTotal=%d, maxBufferedPagesPerConnection=%d, opts=%+v",
|
||||||
maxBufferedPagesTotal, maxBufferedPagesPerConnection, opts)
|
maxBufferedPagesTotal, maxBufferedPagesPerConnection, opts)
|
||||||
a.Assembler.AssemblerOptions.MaxBufferedPagesTotal = maxBufferedPagesTotal
|
a.Assembler.AssemblerOptions.MaxBufferedPagesTotal = maxBufferedPagesTotal
|
||||||
a.Assembler.AssemblerOptions.MaxBufferedPagesPerConnection = maxBufferedPagesPerConnection
|
a.Assembler.AssemblerOptions.MaxBufferedPagesPerConnection = maxBufferedPagesPerConnection
|
||||||
|
|||||||
@@ -60,7 +60,18 @@ Capstone Engine: https://www.capstone-engine.org/
|
|||||||
|
|
||||||
static __always_inline __u32 go_crypto_tls_get_fd_from_tcp_conn(struct pt_regs *ctx) {
|
static __always_inline __u32 go_crypto_tls_get_fd_from_tcp_conn(struct pt_regs *ctx) {
|
||||||
struct go_interface conn;
|
struct go_interface conn;
|
||||||
long err = bpf_probe_read(&conn, sizeof(conn), (void*)GO_ABI_INTERNAL_PT_REGS_R1(ctx));
|
long err;
|
||||||
|
__u64 addr;
|
||||||
|
#if defined(bpf_target_arm64)
|
||||||
|
err = bpf_probe_read(&addr, sizeof(addr), (void*)GO_ABI_INTERNAL_PT_REGS_SP(ctx)+0x8);
|
||||||
|
if (err != 0) {
|
||||||
|
return invalid_fd;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
addr = GO_ABI_INTERNAL_PT_REGS_R1(ctx);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
err = bpf_probe_read(&conn, sizeof(conn), (void*)addr);
|
||||||
if (err != 0) {
|
if (err != 0) {
|
||||||
return invalid_fd;
|
return invalid_fd;
|
||||||
}
|
}
|
||||||
@@ -88,14 +99,23 @@ static __always_inline void go_crypto_tls_uprobe(struct pt_regs *ctx, struct bpf
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct ssl_info info = new_ssl_info();
|
struct ssl_info info = new_ssl_info();
|
||||||
|
long err;
|
||||||
|
|
||||||
|
#if defined(bpf_target_arm64)
|
||||||
|
err = bpf_probe_read(&info.buffer_len, sizeof(__u32), (void*)GO_ABI_INTERNAL_PT_REGS_SP(ctx)+0x18);
|
||||||
|
if (err != 0) {
|
||||||
|
log_error(ctx, LOG_ERROR_READING_BYTES_COUNT, pid_tgid, err, ORIGIN_SSL_UPROBE_CODE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#else
|
||||||
info.buffer_len = GO_ABI_INTERNAL_PT_REGS_R2(ctx);
|
info.buffer_len = GO_ABI_INTERNAL_PT_REGS_R2(ctx);
|
||||||
|
#endif
|
||||||
info.buffer = (void*)GO_ABI_INTERNAL_PT_REGS_R4(ctx);
|
info.buffer = (void*)GO_ABI_INTERNAL_PT_REGS_R4(ctx);
|
||||||
info.fd = go_crypto_tls_get_fd_from_tcp_conn(ctx);
|
info.fd = go_crypto_tls_get_fd_from_tcp_conn(ctx);
|
||||||
|
|
||||||
// GO_ABI_INTERNAL_PT_REGS_GP is Goroutine address
|
// GO_ABI_INTERNAL_PT_REGS_GP is Goroutine address
|
||||||
__u64 pid_fp = pid << 32 | GO_ABI_INTERNAL_PT_REGS_GP(ctx);
|
__u64 pid_fp = pid << 32 | GO_ABI_INTERNAL_PT_REGS_GP(ctx);
|
||||||
long err = bpf_map_update_elem(go_context, &pid_fp, &info, BPF_ANY);
|
err = bpf_map_update_elem(go_context, &pid_fp, &info, BPF_ANY);
|
||||||
|
|
||||||
if (err != 0) {
|
if (err != 0) {
|
||||||
log_error(ctx, LOG_ERROR_PUTTING_SSL_CONTEXT, pid_tgid, err, 0l);
|
log_error(ctx, LOG_ERROR_PUTTING_SSL_CONTEXT, pid_tgid, err, 0l);
|
||||||
@@ -130,7 +150,15 @@ static __always_inline void go_crypto_tls_ex_uprobe(struct pt_regs *ctx, struct
|
|||||||
|
|
||||||
// In case of read, the length is determined on return
|
// In case of read, the length is determined on return
|
||||||
if (flags == FLAGS_IS_READ_BIT) {
|
if (flags == FLAGS_IS_READ_BIT) {
|
||||||
|
#if defined(bpf_target_arm64)
|
||||||
|
// On ARM64 we look at a general-purpose register as an indicator of error return
|
||||||
|
if (GO_ABI_INTERNAL_PT_REGS_R6(ctx) == 0x10) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
info.buffer_len = GO_ABI_INTERNAL_PT_REGS_R7(ctx); // n in return n, nil
|
||||||
|
#else
|
||||||
info.buffer_len = GO_ABI_INTERNAL_PT_REGS_R1(ctx); // n in return n, nil
|
info.buffer_len = GO_ABI_INTERNAL_PT_REGS_R1(ctx); // n in return n, nil
|
||||||
|
#endif
|
||||||
// This check achieves ignoring 0 length reads (the reads result with an error)
|
// This check achieves ignoring 0 length reads (the reads result with an error)
|
||||||
if (info.buffer_len <= 0) {
|
if (info.buffer_len <= 0) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -66,11 +66,12 @@ https://go.googlesource.com/go/+/refs/heads/dev.regabi/src/cmd/compile/internal-
|
|||||||
https://github.com/golang/go/blob/go1.17.6/src/cmd/compile/internal/ssa/gen/AMD64Ops.go#L100
|
https://github.com/golang/go/blob/go1.17.6/src/cmd/compile/internal/ssa/gen/AMD64Ops.go#L100
|
||||||
*/
|
*/
|
||||||
#define GO_ABI_INTERNAL_PT_REGS_R1(x) ((x)->eax)
|
#define GO_ABI_INTERNAL_PT_REGS_R1(x) ((x)->eax)
|
||||||
#define GO_ABI_INTERNAL_PT_REGS_P2(x) ((x)->ecx)
|
#define GO_ABI_INTERNAL_PT_REGS_R2(x) ((x)->ecx)
|
||||||
#define GO_ABI_INTERNAL_PT_REGS_P3(x) ((x)->edx)
|
#define GO_ABI_INTERNAL_PT_REGS_R3(x) ((x)->edx)
|
||||||
#define GO_ABI_INTERNAL_PT_REGS_P4(x) 0
|
#define GO_ABI_INTERNAL_PT_REGS_R4(x) 0
|
||||||
#define GO_ABI_INTERNAL_PT_REGS_P5(x) 0
|
#define GO_ABI_INTERNAL_PT_REGS_R5(x) 0
|
||||||
#define GO_ABI_INTERNAL_PT_REGS_P6(x) 0
|
#define GO_ABI_INTERNAL_PT_REGS_R6(x) 0
|
||||||
|
#define GO_ABI_INTERNAL_PT_REGS_R7(x) 0
|
||||||
#define GO_ABI_INTERNAL_PT_REGS_SP(x) ((x)->esp)
|
#define GO_ABI_INTERNAL_PT_REGS_SP(x) ((x)->esp)
|
||||||
#define GO_ABI_INTERNAL_PT_REGS_FP(x) ((x)->ebp)
|
#define GO_ABI_INTERNAL_PT_REGS_FP(x) ((x)->ebp)
|
||||||
#define GO_ABI_INTERNAL_PT_REGS_GP(x) ((x)->e14)
|
#define GO_ABI_INTERNAL_PT_REGS_GP(x) ((x)->e14)
|
||||||
@@ -83,6 +84,7 @@ https://github.com/golang/go/blob/go1.17.6/src/cmd/compile/internal/ssa/gen/AMD6
|
|||||||
#define GO_ABI_INTERNAL_PT_REGS_R4(x) ((x)->rbx)
|
#define GO_ABI_INTERNAL_PT_REGS_R4(x) ((x)->rbx)
|
||||||
#define GO_ABI_INTERNAL_PT_REGS_R5(x) ((x)->rbp)
|
#define GO_ABI_INTERNAL_PT_REGS_R5(x) ((x)->rbp)
|
||||||
#define GO_ABI_INTERNAL_PT_REGS_R6(x) ((x)->rsi)
|
#define GO_ABI_INTERNAL_PT_REGS_R6(x) ((x)->rsi)
|
||||||
|
#define GO_ABI_INTERNAL_PT_REGS_R7(x) ((x)->rdi)
|
||||||
#define GO_ABI_INTERNAL_PT_REGS_SP(x) ((x)->rsp)
|
#define GO_ABI_INTERNAL_PT_REGS_SP(x) ((x)->rsp)
|
||||||
#define GO_ABI_INTERNAL_PT_REGS_FP(x) ((x)->rbp)
|
#define GO_ABI_INTERNAL_PT_REGS_FP(x) ((x)->rbp)
|
||||||
#define GO_ABI_INTERNAL_PT_REGS_GP(x) ((x)->r14)
|
#define GO_ABI_INTERNAL_PT_REGS_GP(x) ((x)->r14)
|
||||||
@@ -101,7 +103,8 @@ https://github.com/golang/go/blob/go1.17.6/src/cmd/compile/internal/ssa/gen/ARM6
|
|||||||
#define GO_ABI_INTERNAL_PT_REGS_R4(x) ((x)->uregs[3])
|
#define GO_ABI_INTERNAL_PT_REGS_R4(x) ((x)->uregs[3])
|
||||||
#define GO_ABI_INTERNAL_PT_REGS_R5(x) ((x)->uregs[4])
|
#define GO_ABI_INTERNAL_PT_REGS_R5(x) ((x)->uregs[4])
|
||||||
#define GO_ABI_INTERNAL_PT_REGS_R6(x) ((x)->uregs[5])
|
#define GO_ABI_INTERNAL_PT_REGS_R6(x) ((x)->uregs[5])
|
||||||
#define GO_ABI_INTERNAL_PT_REGS_SP(x) ((x)->uregs[14])
|
#define GO_ABI_INTERNAL_PT_REGS_R7(x) ((x)->uregs[6])
|
||||||
|
#define GO_ABI_INTERNAL_PT_REGS_SP(x) ((x)->uregs[13])
|
||||||
#define GO_ABI_INTERNAL_PT_REGS_FP(x) ((x)->uregs[29])
|
#define GO_ABI_INTERNAL_PT_REGS_FP(x) ((x)->uregs[29])
|
||||||
#define GO_ABI_INTERNAL_PT_REGS_GP(x) ((x)->uregs[28])
|
#define GO_ABI_INTERNAL_PT_REGS_GP(x) ((x)->uregs[28])
|
||||||
|
|
||||||
@@ -116,7 +119,8 @@ struct pt_regs;
|
|||||||
#define GO_ABI_INTERNAL_PT_REGS_R4(x) (((PT_REGS_ARM64 *)(x))->regs[3])
|
#define GO_ABI_INTERNAL_PT_REGS_R4(x) (((PT_REGS_ARM64 *)(x))->regs[3])
|
||||||
#define GO_ABI_INTERNAL_PT_REGS_R5(x) (((PT_REGS_ARM64 *)(x))->regs[4])
|
#define GO_ABI_INTERNAL_PT_REGS_R5(x) (((PT_REGS_ARM64 *)(x))->regs[4])
|
||||||
#define GO_ABI_INTERNAL_PT_REGS_R6(x) (((PT_REGS_ARM64 *)(x))->regs[5])
|
#define GO_ABI_INTERNAL_PT_REGS_R6(x) (((PT_REGS_ARM64 *)(x))->regs[5])
|
||||||
#define GO_ABI_INTERNAL_PT_REGS_SP(x) (((PT_REGS_ARM64 *)(x))->regs[30])
|
#define GO_ABI_INTERNAL_PT_REGS_R7(x) (((PT_REGS_ARM64 *)(x))->regs[6])
|
||||||
|
#define GO_ABI_INTERNAL_PT_REGS_SP(x) (((PT_REGS_ARM64 *)(x))->sp)
|
||||||
#define GO_ABI_INTERNAL_PT_REGS_FP(x) (((PT_REGS_ARM64 *)(x))->regs[29])
|
#define GO_ABI_INTERNAL_PT_REGS_FP(x) (((PT_REGS_ARM64 *)(x))->regs[29])
|
||||||
#define GO_ABI_INTERNAL_PT_REGS_GP(x) (((PT_REGS_ARM64 *)(x))->regs[28])
|
#define GO_ABI_INTERNAL_PT_REGS_GP(x) (((PT_REGS_ARM64 *)(x))->regs[28])
|
||||||
|
|
||||||
@@ -132,6 +136,7 @@ https://github.com/golang/go/blob/go1.17.6/src/cmd/compile/internal/ssa/gen/PPC6
|
|||||||
#define GO_ABI_INTERNAL_PT_REGS_R4(x) ((x)->gpr[6])
|
#define GO_ABI_INTERNAL_PT_REGS_R4(x) ((x)->gpr[6])
|
||||||
#define GO_ABI_INTERNAL_PT_REGS_R5(x) ((x)->gpr[7])
|
#define GO_ABI_INTERNAL_PT_REGS_R5(x) ((x)->gpr[7])
|
||||||
#define GO_ABI_INTERNAL_PT_REGS_R6(x) ((x)->gpr[8])
|
#define GO_ABI_INTERNAL_PT_REGS_R6(x) ((x)->gpr[8])
|
||||||
|
#define GO_ABI_INTERNAL_PT_REGS_R7(x) ((x)->gpr[9])
|
||||||
#define GO_ABI_INTERNAL_PT_REGS_SP(x) ((x)->sp)
|
#define GO_ABI_INTERNAL_PT_REGS_SP(x) ((x)->sp)
|
||||||
#define GO_ABI_INTERNAL_PT_REGS_FP(x) ((x)->gpr[12])
|
#define GO_ABI_INTERNAL_PT_REGS_FP(x) ((x)->gpr[12])
|
||||||
#define GO_ABI_INTERNAL_PT_REGS_GP(x) ((x)->gpr[30])
|
#define GO_ABI_INTERNAL_PT_REGS_GP(x) ((x)->gpr[30])
|
||||||
|
|||||||
@@ -48,8 +48,7 @@ static __always_inline void ssl_uprobe(struct pt_regs *ctx, void* ssl, void* buf
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ssl_info *infoPtr = bpf_map_lookup_elem(map_fd, &id);
|
struct ssl_info info = lookup_ssl_info(ctx, map_fd, id);
|
||||||
struct ssl_info info = lookup_ssl_info(ctx, &openssl_write_context, id);
|
|
||||||
|
|
||||||
info.count_ptr = count_ptr;
|
info.count_ptr = count_ptr;
|
||||||
info.buffer = buffer;
|
info.buffer = buffer;
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ func getOffsets(filePath string) (offsets map[string]*goExtendedOffset, err erro
|
|||||||
case "arm64":
|
case "arm64":
|
||||||
engine, err = gapstone.New(
|
engine, err = gapstone.New(
|
||||||
gapstone.CS_ARCH_ARM64,
|
gapstone.CS_ARCH_ARM64,
|
||||||
gapstone.CS_MODE_ARM,
|
gapstone.CS_MODE_LITTLE_ENDIAN,
|
||||||
)
|
)
|
||||||
default:
|
default:
|
||||||
err = fmt.Errorf("Unsupported architecture: %v", runtime.GOARCH)
|
err = fmt.Errorf("Unsupported architecture: %v", runtime.GOARCH)
|
||||||
@@ -86,6 +86,16 @@ func getOffsets(filePath string) (offsets map[string]*goExtendedOffset, err erro
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
engineMajor, engineMinor := engine.Version()
|
||||||
|
logger.Log.Infof(
|
||||||
|
"Disassembling %s with Capstone %d.%d (arch: %d, mode: %d)",
|
||||||
|
filePath,
|
||||||
|
engineMajor,
|
||||||
|
engineMinor,
|
||||||
|
engine.Arch(),
|
||||||
|
engine.Mode(),
|
||||||
|
)
|
||||||
|
|
||||||
offsets = make(map[string]*goExtendedOffset)
|
offsets = make(map[string]*goExtendedOffset)
|
||||||
var fd *os.File
|
var fd *os.File
|
||||||
fd, err = os.Open(filePath)
|
fd, err = os.Open(filePath)
|
||||||
@@ -133,7 +143,7 @@ func getOffsets(filePath string) (offsets map[string]*goExtendedOffset, err erro
|
|||||||
extendedOffset := &goExtendedOffset{enter: offset}
|
extendedOffset := &goExtendedOffset{enter: offset}
|
||||||
|
|
||||||
// source: https://gist.github.com/grantseltzer/3efa8ecc5de1fb566e8091533050d608
|
// source: https://gist.github.com/grantseltzer/3efa8ecc5de1fb566e8091533050d608
|
||||||
// skip over any symbols that aren't functinons/methods
|
// skip over any symbols that aren't functions/methods
|
||||||
if sym.Info != byte(2) && sym.Info != byte(18) {
|
if sym.Info != byte(2) && sym.Info != byte(18) {
|
||||||
offsets[sym.Name] = extendedOffset
|
offsets[sym.Name] = extendedOffset
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -193,7 +193,8 @@ func (t *TlsTapper) tapGoPid(procfs string, pid uint32, namespace string) error
|
|||||||
hooks := goHooks{}
|
hooks := goHooks{}
|
||||||
|
|
||||||
if err := hooks.installUprobes(&t.bpfObjects, exePath); err != nil {
|
if err := hooks.installUprobes(&t.bpfObjects, exePath); err != nil {
|
||||||
return err
|
logger.Log.Infof("PID skipped not a Go binary or symbol table is stripped (pid: %v) %v", pid, exePath)
|
||||||
|
return nil // hide the error on purpose, its OK for a process to be not a Go binary or stripped Go binary
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Log.Infof("Tapping TLS (pid: %v) (Go: %v)", pid, exePath)
|
logger.Log.Infof("Tapping TLS (pid: %v) (Go: %v)", pid, exePath)
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
"@mui/styles": "^5.8.0",
|
"@mui/styles": "^5.8.0",
|
||||||
"@types/lodash": "^4.14.182",
|
"@types/lodash": "^4.14.182",
|
||||||
"@uiw/react-textarea-code-editor": "^1.6.0",
|
"@uiw/react-textarea-code-editor": "^1.6.0",
|
||||||
|
"ace-builds": "^1.6.0",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"core-js": "^3.22.7",
|
"core-js": "^3.22.7",
|
||||||
"highlight.js": "^11.5.1",
|
"highlight.js": "^11.5.1",
|
||||||
@@ -54,6 +55,7 @@
|
|||||||
"node-fetch": "^3.2.4",
|
"node-fetch": "^3.2.4",
|
||||||
"numeral": "^2.0.6",
|
"numeral": "^2.0.6",
|
||||||
"protobuf-decoder": "^0.1.2",
|
"protobuf-decoder": "^0.1.2",
|
||||||
|
"react-ace": "^9.0.0",
|
||||||
"react-graph-vis": "^1.0.7",
|
"react-graph-vis": "^1.0.7",
|
||||||
"react-lowlight": "^3.0.0",
|
"react-lowlight": "^3.0.0",
|
||||||
"react-router-dom": "^6.3.0",
|
"react-router-dom": "^6.3.0",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import makeStyles from '@mui/styles/makeStyles';
|
|||||||
import Protocol from "../UI/Protocol/Protocol"
|
import Protocol from "../UI/Protocol/Protocol"
|
||||||
import Queryable from "../UI/Queryable/Queryable";
|
import Queryable from "../UI/Queryable/Queryable";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { RecoilState, useRecoilValue } from "recoil";
|
import { RecoilState, useRecoilState, useRecoilValue } from "recoil";
|
||||||
import focusedEntryIdAtom from "../../recoil/focusedEntryId";
|
import focusedEntryIdAtom from "../../recoil/focusedEntryId";
|
||||||
import TrafficViewerApi from "../TrafficViewer/TrafficViewerApi";
|
import TrafficViewerApi from "../TrafficViewer/TrafficViewerApi";
|
||||||
import TrafficViewerApiAtom from "../../recoil/TrafficViewerApi/atom";
|
import TrafficViewerApiAtom from "../../recoil/TrafficViewerApi/atom";
|
||||||
@@ -13,6 +13,7 @@ import queryAtom from "../../recoil/query/atom";
|
|||||||
import useWindowDimensions, { useRequestTextByWidth } from "../../hooks/WindowDimensionsHook";
|
import useWindowDimensions, { useRequestTextByWidth } from "../../hooks/WindowDimensionsHook";
|
||||||
import { TOAST_CONTAINER_ID } from "../../configs/Consts";
|
import { TOAST_CONTAINER_ID } from "../../configs/Consts";
|
||||||
import spinner from "assets/spinner.svg";
|
import spinner from "assets/spinner.svg";
|
||||||
|
import entryDataAtom from "../../recoil/entryData";
|
||||||
|
|
||||||
const useStyles = makeStyles(() => ({
|
const useStyles = makeStyles(() => ({
|
||||||
entryTitle: {
|
entryTitle: {
|
||||||
@@ -107,7 +108,7 @@ export const EntryDetailed = () => {
|
|||||||
const trafficViewerApi = useRecoilValue(TrafficViewerApiAtom as RecoilState<TrafficViewerApi>)
|
const trafficViewerApi = useRecoilValue(TrafficViewerApiAtom as RecoilState<TrafficViewerApi>)
|
||||||
const query = useRecoilValue(queryAtom);
|
const query = useRecoilValue(queryAtom);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [entryData, setEntryData] = useState(null);
|
const [entryData, setEntryData] = useRecoilState(entryDataAtom)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setEntryData(null);
|
setEntryData(null);
|
||||||
@@ -147,10 +148,6 @@ export const EntryDetailed = () => {
|
|||||||
representation={entryData.representation}
|
representation={entryData.representation}
|
||||||
isRulesEnabled={entryData.isRulesEnabled}
|
isRulesEnabled={entryData.isRulesEnabled}
|
||||||
rulesMatched={entryData.rulesMatched}
|
rulesMatched={entryData.rulesMatched}
|
||||||
contractStatus={entryData.data.contractStatus}
|
|
||||||
requestReason={entryData.data.contractRequestReason}
|
|
||||||
responseReason={entryData.data.contractResponseReason}
|
|
||||||
contractContent={entryData.data.contractContent}
|
|
||||||
elapsedTime={entryData.data.elapsedTime}
|
elapsedTime={entryData.data.elapsedTime}
|
||||||
color={entryData.protocol.backgroundColor}
|
color={entryData.protocol.backgroundColor}
|
||||||
/>}
|
/>}
|
||||||
|
|||||||
@@ -117,6 +117,39 @@ interface EntryBodySectionProps {
|
|||||||
selector?: string,
|
selector?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const formatRequest = (body: any, contentType: string, decodeBase64: boolean = true, isBase64Encoding: boolean = false, isPretty: boolean = true): string => {
|
||||||
|
if (!decodeBase64 || !body) return body;
|
||||||
|
|
||||||
|
const chunk = body.slice(0, MAXIMUM_BYTES_TO_FORMAT);
|
||||||
|
const bodyBuf = isBase64Encoding ? atob(chunk) : chunk;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (jsonLikeFormats.some(format => contentType?.indexOf(format) > -1)) {
|
||||||
|
if (!isPretty) return bodyBuf;
|
||||||
|
return jsonBeautify(JSON.parse(bodyBuf), null, 2, 80);
|
||||||
|
} else if (xmlLikeFormats.some(format => contentType?.indexOf(format) > -1)) {
|
||||||
|
if (!isPretty) return bodyBuf;
|
||||||
|
return xmlBeautify(bodyBuf, {
|
||||||
|
indentation: ' ',
|
||||||
|
filter: (node) => node.type !== 'Comment',
|
||||||
|
collapseContent: true,
|
||||||
|
lineSeparator: '\n'
|
||||||
|
});
|
||||||
|
} else if (protobufFormats.some(format => contentType?.indexOf(format) > -1)) {
|
||||||
|
// Replace all non printable characters (ASCII)
|
||||||
|
const protobufDecoder = new ProtobufDecoder(bodyBuf, true);
|
||||||
|
const protobufDecoded = protobufDecoder.decode().toSimple();
|
||||||
|
if (!isPretty) return JSON.stringify(protobufDecoded);
|
||||||
|
return jsonBeautify(protobufDecoded, null, 2, 80);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return bodyBuf;
|
||||||
|
}
|
||||||
|
|
||||||
export const EntryBodySection: React.FC<EntryBodySectionProps> = ({
|
export const EntryBodySection: React.FC<EntryBodySectionProps> = ({
|
||||||
title,
|
title,
|
||||||
color,
|
color,
|
||||||
@@ -139,42 +172,17 @@ export const EntryBodySection: React.FC<EntryBodySectionProps> = ({
|
|||||||
!isLineNumbersGreaterThenOne && setShowLineNumbers(false);
|
!isLineNumbersGreaterThenOne && setShowLineNumbers(false);
|
||||||
}, [isLineNumbersGreaterThenOne, isPretty])
|
}, [isLineNumbersGreaterThenOne, isPretty])
|
||||||
|
|
||||||
const formatTextBody = useCallback((body: any): string => {
|
const formatTextBody = useCallback((body) => {
|
||||||
if (!decodeBase64) return body;
|
|
||||||
|
|
||||||
const chunk = body.slice(0, MAXIMUM_BYTES_TO_FORMAT);
|
|
||||||
const bodyBuf = isBase64Encoding ? atob(chunk) : chunk;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (jsonLikeFormats.some(format => contentType?.indexOf(format) > -1)) {
|
return formatRequest(body, contentType, decodeBase64, isBase64Encoding, isPretty)
|
||||||
if (!isPretty) return bodyBuf;
|
|
||||||
return jsonBeautify(JSON.parse(bodyBuf), null, 2, 80);
|
|
||||||
} else if (xmlLikeFormats.some(format => contentType?.indexOf(format) > -1)) {
|
|
||||||
if (!isPretty) return bodyBuf;
|
|
||||||
return xmlBeautify(bodyBuf, {
|
|
||||||
indentation: ' ',
|
|
||||||
filter: (node) => node.type !== 'Comment',
|
|
||||||
collapseContent: true,
|
|
||||||
lineSeparator: '\n'
|
|
||||||
});
|
|
||||||
} else if (protobufFormats.some(format => contentType?.indexOf(format) > -1)) {
|
|
||||||
// Replace all non printable characters (ASCII)
|
|
||||||
const protobufDecoder = new ProtobufDecoder(bodyBuf, true);
|
|
||||||
const protobufDecoded = protobufDecoder.decode().toSimple();
|
|
||||||
if (!isPretty) return JSON.stringify(protobufDecoded);
|
|
||||||
return jsonBeautify(protobufDecoded, null, 2, 80);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (String(error).includes("More than one message in")) {
|
if (String(error).includes("More than one message in")) {
|
||||||
if (isDecodeGrpc)
|
if (isDecodeGrpc)
|
||||||
setIsDecodeGrpc(false);
|
setIsDecodeGrpc(false);
|
||||||
} else if (String(error).includes("Failed to parse")) {
|
} else if (String(error).includes("Failed to parse")) {
|
||||||
console.warn(error);
|
console.warn(error);
|
||||||
} else {
|
|
||||||
console.error(error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return bodyBuf;
|
|
||||||
}, [isPretty, contentType, isDecodeGrpc, decodeBase64, isBase64Encoding])
|
}, [isPretty, contentType, isDecodeGrpc, decodeBase64, isBase64Encoding])
|
||||||
|
|
||||||
const formattedText = useMemo(() => formatTextBody(content), [formatTextBody, content]);
|
const formattedText = useMemo(() => formatTextBody(content), [formatTextBody, content]);
|
||||||
@@ -364,27 +372,3 @@ export const EntryTablePolicySection: React.FC<EntryPolicySectionProps> = ({ tit
|
|||||||
}
|
}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EntryContractSectionProps {
|
|
||||||
color: string,
|
|
||||||
requestReason: string,
|
|
||||||
responseReason: string,
|
|
||||||
contractContent: string,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EntryContractSection: React.FC<EntryContractSectionProps> = ({ color, requestReason, responseReason, contractContent }) => {
|
|
||||||
return <React.Fragment>
|
|
||||||
{requestReason && <EntrySectionContainer title="Request" color={color}>
|
|
||||||
{requestReason}
|
|
||||||
</EntrySectionContainer>}
|
|
||||||
{responseReason && <EntrySectionContainer title="Response" color={color}>
|
|
||||||
{responseReason}
|
|
||||||
</EntrySectionContainer>}
|
|
||||||
{contractContent && <EntrySectionContainer title="Contract" color={color}>
|
|
||||||
<SyntaxHighlighter
|
|
||||||
code={contractContent}
|
|
||||||
language={"yaml"}
|
|
||||||
/>
|
|
||||||
</EntrySectionContainer>}
|
|
||||||
</React.Fragment>
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import React, { useState, useCallback } from "react"
|
||||||
|
import { useRecoilValue, useSetRecoilState } from "recoil"
|
||||||
|
import entryDataAtom from "../../../recoil/entryData"
|
||||||
|
import SectionsRepresentation from "./SectionsRepresentation";
|
||||||
|
import { EntryTablePolicySection } from "../EntrySections/EntrySections";
|
||||||
|
import { ReactComponent as ReplayIcon } from './replay.svg';
|
||||||
|
import styles from './EntryViewer.module.sass';
|
||||||
|
import { Tabs } from "../../UI";
|
||||||
|
import replayRequestModalOpenAtom from "../../../recoil/replayRequestModalOpen";
|
||||||
|
|
||||||
|
const enabledProtocolsForReplay = ["http"]
|
||||||
|
|
||||||
|
export const AutoRepresentation: React.FC<any> = ({ representation, isRulesEnabled, rulesMatched, elapsedTime, color, isDisplayReplay = false }) => {
|
||||||
|
const entryData = useRecoilValue(entryDataAtom)
|
||||||
|
const setIsOpenRequestModal = useSetRecoilState(replayRequestModalOpenAtom)
|
||||||
|
const isReplayDisplayed = useCallback(() => {
|
||||||
|
return enabledProtocolsForReplay.find(x => x === entryData.protocol.name) && isDisplayReplay
|
||||||
|
}, [entryData.protocol.name, isDisplayReplay])
|
||||||
|
|
||||||
|
const TABS = [
|
||||||
|
{
|
||||||
|
tab: 'Request',
|
||||||
|
badge: isReplayDisplayed() && <span title="Replay Request"><ReplayIcon fill={color} stroke={color} style={{ marginLeft: "10px", cursor: "pointer", height: "22px" }} onClick={() => setIsOpenRequestModal(true)} /></span>
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const [currentTab, setCurrentTab] = useState(TABS[0].tab);
|
||||||
|
|
||||||
|
// Don't fail even if `representation` is an empty string
|
||||||
|
if (!representation) {
|
||||||
|
return <React.Fragment></React.Fragment>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { request, response } = JSON.parse(representation);
|
||||||
|
|
||||||
|
let responseTabIndex = 0;
|
||||||
|
let rulesTabIndex = 0;
|
||||||
|
|
||||||
|
if (response) {
|
||||||
|
TABS.push(
|
||||||
|
{
|
||||||
|
tab: 'Response',
|
||||||
|
badge: null
|
||||||
|
}
|
||||||
|
);
|
||||||
|
responseTabIndex = TABS.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRulesEnabled) {
|
||||||
|
TABS.push(
|
||||||
|
{
|
||||||
|
tab: 'Rules',
|
||||||
|
badge: null
|
||||||
|
}
|
||||||
|
);
|
||||||
|
rulesTabIndex = TABS.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className={styles.Entry}>
|
||||||
|
{<div className={styles.body}>
|
||||||
|
<div className={styles.bodyHeader}>
|
||||||
|
<Tabs tabs={TABS} currentTab={currentTab} color={color} onChange={setCurrentTab} leftAligned />
|
||||||
|
</div>
|
||||||
|
{currentTab === TABS[0].tab && <React.Fragment>
|
||||||
|
<SectionsRepresentation data={request} color={color} requestRepresentation={request} />
|
||||||
|
</React.Fragment>}
|
||||||
|
{response && currentTab === TABS[responseTabIndex].tab && <React.Fragment>
|
||||||
|
<SectionsRepresentation data={response} color={color} />
|
||||||
|
</React.Fragment>}
|
||||||
|
{isRulesEnabled && currentTab === TABS[rulesTabIndex].tab && <React.Fragment>
|
||||||
|
<EntryTablePolicySection title={'Rule'} color={color} latency={elapsedTime} arrayToIterate={rulesMatched ? rulesMatched : []} />
|
||||||
|
</React.Fragment>}
|
||||||
|
</div>}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
@@ -1,128 +1,22 @@
|
|||||||
import React, {useState} from 'react';
|
import React from 'react';
|
||||||
import styles from './EntryViewer.module.sass';
|
import { AutoRepresentation } from './AutoRepresentation';
|
||||||
import Tabs from "../../UI/Tabs/Tabs";
|
|
||||||
import {EntryTableSection, EntryBodySection, EntryTablePolicySection, EntryContractSection} from "../EntrySections/EntrySections";
|
|
||||||
|
|
||||||
enum SectionTypes {
|
|
||||||
SectionTable = "table",
|
|
||||||
SectionBody = "body",
|
|
||||||
}
|
|
||||||
|
|
||||||
const SectionsRepresentation: React.FC<any> = ({data, color}) => {
|
|
||||||
const sections = []
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
for (const [i, row] of data.entries()) {
|
|
||||||
switch (row.type) {
|
|
||||||
case SectionTypes.SectionTable:
|
|
||||||
sections.push(
|
|
||||||
<EntryTableSection key={i} title={row.title} color={color} arrayToIterate={JSON.parse(row.data)}/>
|
|
||||||
)
|
|
||||||
break;
|
|
||||||
case SectionTypes.SectionBody:
|
|
||||||
sections.push(
|
|
||||||
<EntryBodySection key={i} title={row.title} color={color} content={row.data} encoding={row.encoding} contentType={row.mimeType} selector={row.selector}/>
|
|
||||||
)
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return <React.Fragment>{sections}</React.Fragment>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const AutoRepresentation: React.FC<any> = ({representation, isRulesEnabled, rulesMatched, contractStatus, requestReason, responseReason, contractContent, elapsedTime, color}) => {
|
|
||||||
var TABS = [
|
|
||||||
{
|
|
||||||
tab: 'Request'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
const [currentTab, setCurrentTab] = useState(TABS[0].tab);
|
|
||||||
|
|
||||||
// Don't fail even if `representation` is an empty string
|
|
||||||
if (!representation) {
|
|
||||||
return <React.Fragment></React.Fragment>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {request, response} = JSON.parse(representation);
|
|
||||||
|
|
||||||
let responseTabIndex = 0;
|
|
||||||
let rulesTabIndex = 0;
|
|
||||||
let contractTabIndex = 0;
|
|
||||||
|
|
||||||
if (response) {
|
|
||||||
TABS.push(
|
|
||||||
{
|
|
||||||
tab: 'Response',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
responseTabIndex = TABS.length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isRulesEnabled) {
|
|
||||||
TABS.push(
|
|
||||||
{
|
|
||||||
tab: 'Rules',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
rulesTabIndex = TABS.length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (contractStatus !== 0 && contractContent) {
|
|
||||||
TABS.push(
|
|
||||||
{
|
|
||||||
tab: 'Contract',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
contractTabIndex = TABS.length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className={styles.Entry}>
|
|
||||||
{<div className={styles.body}>
|
|
||||||
<div className={styles.bodyHeader}>
|
|
||||||
<Tabs tabs={TABS} currentTab={currentTab} color={color} onChange={setCurrentTab} leftAligned/>
|
|
||||||
</div>
|
|
||||||
{currentTab === TABS[0].tab && <React.Fragment>
|
|
||||||
<SectionsRepresentation data={request} color={color}/>
|
|
||||||
</React.Fragment>}
|
|
||||||
{response && currentTab === TABS[responseTabIndex].tab && <React.Fragment>
|
|
||||||
<SectionsRepresentation data={response} color={color}/>
|
|
||||||
</React.Fragment>}
|
|
||||||
{isRulesEnabled && currentTab === TABS[rulesTabIndex].tab && <React.Fragment>
|
|
||||||
<EntryTablePolicySection title={'Rule'} color={color} latency={elapsedTime} arrayToIterate={rulesMatched ? rulesMatched : []}/>
|
|
||||||
</React.Fragment>}
|
|
||||||
{contractStatus !== 0 && contractContent && currentTab === TABS[contractTabIndex].tab && <React.Fragment>
|
|
||||||
<EntryContractSection color={color} requestReason={requestReason} responseReason={responseReason} contractContent={contractContent}/>
|
|
||||||
</React.Fragment>}
|
|
||||||
</div>}
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
representation: any;
|
representation: any;
|
||||||
isRulesEnabled: boolean;
|
isRulesEnabled: boolean;
|
||||||
rulesMatched: any;
|
rulesMatched: any;
|
||||||
contractStatus: number;
|
|
||||||
requestReason: string;
|
|
||||||
responseReason: string;
|
|
||||||
contractContent: string;
|
|
||||||
color: string;
|
color: string;
|
||||||
elapsedTime: number;
|
elapsedTime: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EntryViewer: React.FC<Props> = ({representation, isRulesEnabled, rulesMatched, contractStatus, requestReason, responseReason, contractContent, elapsedTime, color}) => {
|
const EntryViewer: React.FC<Props> = ({ representation, isRulesEnabled, rulesMatched, elapsedTime, color }) => {
|
||||||
return <AutoRepresentation
|
return <AutoRepresentation
|
||||||
representation={representation}
|
representation={representation}
|
||||||
isRulesEnabled={isRulesEnabled}
|
isRulesEnabled={isRulesEnabled}
|
||||||
rulesMatched={rulesMatched}
|
rulesMatched={rulesMatched}
|
||||||
contractStatus={contractStatus}
|
|
||||||
requestReason={requestReason}
|
|
||||||
responseReason={responseReason}
|
|
||||||
contractContent={contractContent}
|
|
||||||
elapsedTime={elapsedTime}
|
elapsedTime={elapsedTime}
|
||||||
color={color}
|
color={color}
|
||||||
|
isDisplayReplay={true}
|
||||||
/>
|
/>
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { EntryTableSection, EntryBodySection } from "../EntrySections/EntrySections";
|
||||||
|
|
||||||
|
enum SectionTypes {
|
||||||
|
SectionTable = "table",
|
||||||
|
SectionBody = "body",
|
||||||
|
}
|
||||||
|
|
||||||
|
const SectionsRepresentation: React.FC<any> = ({ data, color }) => {
|
||||||
|
const sections = []
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
for (const [i, row] of data.entries()) {
|
||||||
|
switch (row.type) {
|
||||||
|
case SectionTypes.SectionTable:
|
||||||
|
sections.push(
|
||||||
|
<EntryTableSection key={i} title={row.title} color={color} arrayToIterate={JSON.parse(row.data)} />
|
||||||
|
)
|
||||||
|
break;
|
||||||
|
case SectionTypes.SectionBody:
|
||||||
|
sections.push(
|
||||||
|
<EntryBodySection key={i} title={row.title} color={color} content={row.data} encoding={row.encoding} contentType={row.mimeType} selector={row.selector} />
|
||||||
|
)
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <React.Fragment>{sections}</React.Fragment>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SectionsRepresentation
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" ?><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title/><path d="M16,12a1,1,0,0,1-.49.86l-5,3A1,1,0,0,1,10,16a1,1,0,0,1-.49-.13A1,1,0,0,1,9,15V9a1,1,0,0,1,1.51-.86l5,3A1,1,0,0,1,16,12Z" fill="#464646"/><path d="M21.92,5.09a1,1,0,0,0-1.07.15L19.94,6A9.84,9.84,0,0,0,12,2a10,10,0,1,0,9.42,13.33,1,1,0,0,0-1.89-.66A8,8,0,1,1,12,4a7.87,7.87,0,0,1,6.42,3.32l-1.07.92A1,1,0,0,0,18,10h3.5a1,1,0,0,0,1-1V6A1,1,0,0,0,21.92,5.09Z" fill="#464646"/></svg>
|
||||||
|
After Width: | Height: | Size: 477 B |
4
ui-common/src/components/EntryDetailed/assets/run.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="15" cy="15" r="13.5" stroke="#205CF5" stroke-width="3"/>
|
||||||
|
<path d="M20 15C20 15.3167 19.8392 15.6335 19.5175 15.8189L12.5051 19.8624C11.8427 20.2444 11 19.7858 11 19.0435V10.9565C11 10.2142 11.8427 9.75564 12.5051 10.1376L19.5175 14.1811C19.8392 14.3665 20 14.6833 20 15Z" fill="#205CF5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 404 B |
@@ -38,7 +38,6 @@ interface Entry {
|
|||||||
isOutgoing?: boolean;
|
isOutgoing?: boolean;
|
||||||
latency: number;
|
latency: number;
|
||||||
rules: Rules;
|
rules: Rules;
|
||||||
contractStatus: number,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Rules {
|
interface Rules {
|
||||||
@@ -117,26 +116,6 @@ export const EntryItem: React.FC<EntryProps> = ({entry, style, headingMode, name
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let contractEnabled = true;
|
|
||||||
let contractText = "";
|
|
||||||
switch (entry.contractStatus) {
|
|
||||||
case 0:
|
|
||||||
contractEnabled = false;
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
additionalRulesProperties = styles.ruleSuccessRow
|
|
||||||
ruleSuccess = true
|
|
||||||
contractText = "No Breaches"
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
additionalRulesProperties = styles.ruleFailureRow
|
|
||||||
ruleSuccess = false
|
|
||||||
contractText = "Breach"
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const isStatusCodeEnabled = ((entry.proto.name === "http" && "status" in entry) || entry.status !== 0);
|
const isStatusCodeEnabled = ((entry.proto.name === "http" && "status" in entry) || entry.status !== 0);
|
||||||
|
|
||||||
@@ -144,7 +123,7 @@ export const EntryItem: React.FC<EntryProps> = ({entry, style, headingMode, name
|
|||||||
<div
|
<div
|
||||||
id={`entry-${entry.id}`}
|
id={`entry-${entry.id}`}
|
||||||
className={`${styles.row}
|
className={`${styles.row}
|
||||||
${isSelected && !rule && !contractEnabled ? styles.rowSelected : additionalRulesProperties}`}
|
${isSelected && !rule ? styles.rowSelected : additionalRulesProperties}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!setFocusedEntryId) return;
|
if (!setFocusedEntryId) return;
|
||||||
setFocusedEntryId(entry.id);
|
setFocusedEntryId(entry.id);
|
||||||
@@ -210,18 +189,11 @@ export const EntryItem: React.FC<EntryProps> = ({entry, style, headingMode, name
|
|||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
rule ?
|
rule ?
|
||||||
<div className={`${styles.ruleNumberText} ${ruleSuccess ? styles.ruleNumberTextSuccess : styles.ruleNumberTextFailure} ${rule && contractEnabled ? styles.separatorRight : ""}`}>
|
<div className={`${styles.ruleNumberText} ${ruleSuccess ? styles.ruleNumberTextSuccess : styles.ruleNumberTextFailure} ${rule ? styles.separatorRight : ""}`}>
|
||||||
{`Rules (${numberOfRules})`}
|
{`Rules (${numberOfRules})`}
|
||||||
</div>
|
</div>
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
{
|
|
||||||
contractEnabled ?
|
|
||||||
<div className={`${styles.ruleNumberText} ${ruleSuccess ? styles.ruleNumberTextSuccess : styles.ruleNumberTextFailure} ${rule && contractEnabled ? styles.separatorLeft : ""}`}>
|
|
||||||
{contractText}
|
|
||||||
</div>
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
<div className={styles.separatorRight}>
|
<div className={styles.separatorRight}>
|
||||||
{headingMode ? <Queryable
|
{headingMode ? <Queryable
|
||||||
query={`namespace == "${namespace}"`}
|
query={`namespace == "${namespace}"`}
|
||||||
|
|||||||
@@ -88,8 +88,17 @@
|
|||||||
.greenIndicatorContainer
|
.greenIndicatorContainer
|
||||||
border: 2px #6fcf9770 solid
|
border: 2px #6fcf9770 solid
|
||||||
|
|
||||||
|
@keyframes biggerIndication
|
||||||
|
0%
|
||||||
|
transform: scale(2.0)
|
||||||
|
100%
|
||||||
|
transform: scale(0.7)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.greenIndicator
|
.greenIndicator
|
||||||
background-color: #27AE60
|
background-color: #27AE60
|
||||||
|
animation: biggerIndication 1.5s ease-out 0s alternate infinite none running
|
||||||
|
|
||||||
.orangeIndicatorContainer
|
.orangeIndicatorContainer
|
||||||
border: 2px #fabd5970 solid
|
border: 2px #fabd5970 solid
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import tappingStatusAtom from "../../recoil/tappingStatus/atom";
|
|||||||
import { TOAST_CONTAINER_ID } from "../../configs/Consts";
|
import { TOAST_CONTAINER_ID } from "../../configs/Consts";
|
||||||
import leftOffTopAtom from "../../recoil/leftOffTop";
|
import leftOffTopAtom from "../../recoil/leftOffTop";
|
||||||
import { DEFAULT_LEFTOFF, DEFAULT_FETCH, DEFAULT_FETCH_TIMEOUT_MS } from '../../hooks/useWS';
|
import { DEFAULT_LEFTOFF, DEFAULT_FETCH, DEFAULT_FETCH_TIMEOUT_MS } from '../../hooks/useWS';
|
||||||
|
import ReplayRequestModalContainer from "../modals/ReplayRequestModal/ReplayRequestModal";
|
||||||
|
|
||||||
const useLayoutStyles = makeStyles(() => ({
|
const useLayoutStyles = makeStyles(() => ({
|
||||||
details: {
|
details: {
|
||||||
@@ -278,6 +279,7 @@ const TrafficViewerContainer: React.FC<TrafficViewerProps> = ({
|
|||||||
pauseOnFocusLoss
|
pauseOnFocusLoss
|
||||||
draggable
|
draggable
|
||||||
pauseOnHover/>
|
pauseOnHover/>
|
||||||
|
<ReplayRequestModalContainer />
|
||||||
</RecoilRoot>
|
</RecoilRoot>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ type TrafficViewerApi = {
|
|||||||
validateQuery: (query: any) => any
|
validateQuery: (query: any) => any
|
||||||
tapStatus: () => any
|
tapStatus: () => any
|
||||||
fetchEntries: (leftOff: any, direction: number, query: any, limit: number, timeoutMs: number) => any
|
fetchEntries: (leftOff: any, direction: number, query: any, limit: number, timeoutMs: number) => any
|
||||||
getEntry: (entryId: any, query: string) => any
|
getEntry: (entryId: any, query: string) => any,
|
||||||
|
replayRequest: (request: { method: string, url: string, data: string, headers: {} }) => Promise<any>,
|
||||||
webSocket: {
|
webSocket: {
|
||||||
close: () => void
|
close: () => void
|
||||||
}
|
}
|
||||||
|
|||||||
54
ui-common/src/components/UI/CodeEditor/CodeEditor.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import React from "react";
|
||||||
|
import AceEditor from "react-ace";
|
||||||
|
import { config } from 'ace-builds';
|
||||||
|
|
||||||
|
import "ace-builds/src-noconflict/ext-searchbox";
|
||||||
|
import "ace-builds/src-noconflict/mode-python";
|
||||||
|
import "ace-builds/src-noconflict/mode-json";
|
||||||
|
import "ace-builds/src-noconflict/theme-github";
|
||||||
|
import "ace-builds/src-noconflict/mode-javascript";
|
||||||
|
import "ace-builds/src-noconflict/mode-xml";
|
||||||
|
import "ace-builds/src-noconflict/mode-html";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
config.set(
|
||||||
|
"basePath",
|
||||||
|
"https://cdn.jsdelivr.net/npm/ace-builds@1.4.6/src-noconflict/"
|
||||||
|
);
|
||||||
|
config.setModuleUrl(
|
||||||
|
"ace/mode/javascript_worker",
|
||||||
|
"https://cdn.jsdelivr.net/npm/ace-builds@1.4.6/src-noconflict/worker-javascript.js"
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface CodeEditorProps {
|
||||||
|
code: string,
|
||||||
|
onChange?: (code: string) => void,
|
||||||
|
language?: string
|
||||||
|
}
|
||||||
|
const CodeEditor: React.FC<CodeEditorProps> = ({
|
||||||
|
language,
|
||||||
|
onChange,
|
||||||
|
code
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<AceEditor
|
||||||
|
mode={language}
|
||||||
|
theme="github"
|
||||||
|
onChange={onChange}
|
||||||
|
editorProps={{ $blockScrolling: true }}
|
||||||
|
setOptions={{
|
||||||
|
enableBasicAutocompletion: true,
|
||||||
|
enableLiveAutocompletion: true,
|
||||||
|
enableSnippets: true
|
||||||
|
}}
|
||||||
|
showPrintMargin={false}
|
||||||
|
value={code}
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
style={{ borderRadius: "inherit" }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CodeEditor
|
||||||
51
ui-common/src/components/UI/HoverImage/HoverImage.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export type HoverImageProps = {
|
||||||
|
src: string;
|
||||||
|
hoverSrc: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
className?: string;
|
||||||
|
style?: any;
|
||||||
|
onClick?: React.MouseEventHandler;
|
||||||
|
alt?: string
|
||||||
|
};
|
||||||
|
const HoverImage: React.FC<HoverImageProps> = ({
|
||||||
|
src,
|
||||||
|
hoverSrc,
|
||||||
|
style,
|
||||||
|
disabled,
|
||||||
|
onClick,
|
||||||
|
className,
|
||||||
|
alt = ""
|
||||||
|
}) => {
|
||||||
|
const [imageSrc, setImageSrc] = React.useState<string>(src);
|
||||||
|
|
||||||
|
const mouseOver = React.useCallback(() => {
|
||||||
|
setImageSrc(hoverSrc);
|
||||||
|
}, [hoverSrc]);
|
||||||
|
|
||||||
|
const mouseOut = React.useCallback(() => {
|
||||||
|
setImageSrc(src);
|
||||||
|
}, [src]);
|
||||||
|
|
||||||
|
const handleClick = (e: React.MouseEvent) => {
|
||||||
|
if (!onClick) return;
|
||||||
|
if (!disabled) {
|
||||||
|
onClick(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
src={imageSrc}
|
||||||
|
style={style}
|
||||||
|
onMouseOver={mouseOver}
|
||||||
|
onMouseOut={mouseOut}
|
||||||
|
onClick={handleClick}
|
||||||
|
className={className}
|
||||||
|
alt={alt}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HoverImage;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
@import '../../../variables.module'
|
||||||
|
|
||||||
|
.keyValueTableContainer
|
||||||
|
width: 100%
|
||||||
|
background-color: inherit
|
||||||
|
border-radius: 4px
|
||||||
|
overflow-x: auto
|
||||||
|
overflow-y: auto
|
||||||
|
height: 100%
|
||||||
|
padding: 10px 0
|
||||||
|
box-sizing: border-box
|
||||||
|
|
||||||
|
.headerRow
|
||||||
|
display: flex
|
||||||
|
margin: 15px
|
||||||
|
align-items: center
|
||||||
|
|
||||||
|
.roundInputContainer
|
||||||
|
background-color: $main-background-color
|
||||||
|
border-radius: 15px
|
||||||
|
margin-right: 5px
|
||||||
|
padding: 5px
|
||||||
|
|
||||||
|
input
|
||||||
|
border: none
|
||||||
|
outline: none
|
||||||
|
background-color: transparent
|
||||||
|
font-size: 15px
|
||||||
|
width: 100%
|
||||||
79
ui-common/src/components/UI/KeyValueTable/KeyValueTable.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import styles from "./KeyValueTable.module.sass"
|
||||||
|
import deleteIcon from "delete.svg"
|
||||||
|
import deleteIconActive from "delete-active.svg"
|
||||||
|
import HoverImage from "../HoverImage/HoverImage";
|
||||||
|
|
||||||
|
interface KeyValueTableProps {
|
||||||
|
data: any
|
||||||
|
onDataChange: (data: any) => void
|
||||||
|
keyPlaceholder?: string
|
||||||
|
valuePlaceholder?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Row = { key: string, value: string }
|
||||||
|
|
||||||
|
const KeyValueTable: React.FC<KeyValueTableProps> = ({ data, onDataChange, keyPlaceholder, valuePlaceholder }) => {
|
||||||
|
|
||||||
|
const [keyValueData, setKeyValueData] = useState([] as Row[])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!data) return;
|
||||||
|
const currentState = [...data, { key: "", value: "" }]
|
||||||
|
setKeyValueData(currentState)
|
||||||
|
}, [data])
|
||||||
|
|
||||||
|
const deleteRow = (index) => {
|
||||||
|
const newRows = [...keyValueData];
|
||||||
|
newRows.splice(index, 1);
|
||||||
|
setKeyValueData(newRows);
|
||||||
|
onDataChange(newRows.filter(row => row.key))
|
||||||
|
}
|
||||||
|
|
||||||
|
const addNewRow = (data: Row[]) => {
|
||||||
|
return data.filter(x => x.key === "").length === 0 ? [...data, { key: '', value: '' }] : data
|
||||||
|
}
|
||||||
|
|
||||||
|
const setNewVal = (mapFunc, index) => {
|
||||||
|
let currentData = keyValueData.map((row, i) => i === index ? mapFunc(row) : row)
|
||||||
|
if (currentData.every(row => row.key)) {
|
||||||
|
onDataChange(currentData)
|
||||||
|
currentData = addNewRow(currentData)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
onDataChange(currentData.filter(row => row.key))
|
||||||
|
}
|
||||||
|
|
||||||
|
setKeyValueData(currentData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className={styles.keyValueTableContainer}>
|
||||||
|
{keyValueData?.map((row, index) => {
|
||||||
|
return <div key={index} className={styles.headerRow}>
|
||||||
|
<div className={styles.roundInputContainer} style={{ width: "30%" }}>
|
||||||
|
<input
|
||||||
|
name="key" type="text"
|
||||||
|
placeholder={keyPlaceholder ? keyPlaceholder : "New key"}
|
||||||
|
onChange={(event) => setNewVal((row) => { return { key: event.target.value, value: row.value } }, index)}
|
||||||
|
value={row.key}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.roundInputContainer} style={{ width: "65%" }}>
|
||||||
|
<input
|
||||||
|
name="value" type="text"
|
||||||
|
placeholder={valuePlaceholder ? valuePlaceholder : "New Value"}
|
||||||
|
onChange={(event) => setNewVal((row) => { return { key: row.key, value: event.target.value } }, index)}
|
||||||
|
value={row.value?.toString()}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false} />
|
||||||
|
</div>
|
||||||
|
{(row.key !== "" || row.value !== "") && <HoverImage alt="delete" style={{ marginLeft: "5px", cursor: "pointer" }} className="deleteIcon" src={deleteIcon}
|
||||||
|
onClick={() => deleteRow(index)} hoverSrc={deleteIconActive} />}
|
||||||
|
</div>
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default KeyValueTable
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="16" height="18" viewBox="0 0 16 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M15.9166 4C15.9166 4.20411 15.8416 4.40111 15.706 4.55364C15.5704 4.70617 15.3835 4.80362 15.1808 4.8275L15.0833 4.83333H14.3791L13.3533 15.2667C13.2974 15.8328 13.033 16.3579 12.6114 16.7399C12.1899 17.1219 11.6413 17.3334 11.0724 17.3333H4.92742C4.35854 17.3334 3.80998 17.1219 3.38842 16.7399C2.96686 16.3579 2.70244 15.8328 2.64659 15.2667L1.62075 4.83333H0.916585C0.695572 4.83333 0.48361 4.74554 0.32733 4.58926C0.171049 4.43298 0.083252 4.22101 0.083252 4C0.083252 3.77899 0.171049 3.56702 0.32733 3.41074C0.48361 3.25446 0.695572 3.16667 0.916585 3.16667H5.08325C5.08325 2.78364 5.15869 2.40437 5.30527 2.05051C5.45185 1.69664 5.66669 1.37511 5.93752 1.10427C6.20836 0.833434 6.52989 0.618594 6.88376 0.472018C7.23763 0.325442 7.6169 0.25 7.99992 0.25C8.38294 0.25 8.76221 0.325442 9.11608 0.472018C9.46994 0.618594 9.79148 0.833434 10.0623 1.10427C10.3332 1.37511 10.548 1.69664 10.6946 2.05051C10.8411 2.40437 10.9166 2.78364 10.9166 3.16667H15.0833C15.3043 3.16667 15.5162 3.25446 15.6725 3.41074C15.8288 3.56702 15.9166 3.77899 15.9166 4ZM9.87492 6.70833C9.72389 6.70834 9.57797 6.76304 9.46414 6.86231C9.35032 6.96158 9.27629 7.09871 9.25575 7.24833L9.24992 7.33333V13.1667L9.25575 13.2517C9.27633 13.4013 9.35038 13.5383 9.4642 13.6376C9.57802 13.7368 9.72392 13.7915 9.87492 13.7915C10.0259 13.7915 10.1718 13.7368 10.2856 13.6376C10.3995 13.5383 10.4735 13.4013 10.4941 13.2517L10.4999 13.1667V7.33333L10.4941 7.24833C10.4735 7.09871 10.3995 6.96158 10.2857 6.86231C10.1719 6.76304 10.0259 6.70834 9.87492 6.70833ZM6.12492 6.70833C5.97389 6.70834 5.82797 6.76304 5.71414 6.86231C5.60032 6.96158 5.52629 7.09871 5.50575 7.24833L5.49992 7.33333V13.1667L5.50575 13.2517C5.52633 13.4013 5.60038 13.5383 5.7142 13.6376C5.82802 13.7368 5.97392 13.7915 6.12492 13.7915C6.27592 13.7915 6.42182 13.7368 6.53564 13.6376C6.64946 13.5383 6.7235 13.4013 6.74409 13.2517L6.74992 13.1667V7.33333L6.74409 7.24833C6.72355 7.09871 6.64952 6.96158 6.53569 6.86231C6.42187 6.76304 6.27595 6.70834 6.12492 6.70833ZM7.99992 1.91667C7.6684 1.91667 7.35046 2.04836 7.11603 2.28278C6.88161 2.5172 6.74992 2.83515 6.74992 3.16667H9.24992C9.24992 2.83515 9.11822 2.5172 8.8838 2.28278C8.64938 2.04836 8.33144 1.91667 7.99992 1.91667Z" fill="#DB2156"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
3
ui-common/src/components/UI/KeyValueTable/delete.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="16" height="18" viewBox="0 0 16 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M15.9166 4C15.9166 4.20411 15.8416 4.40111 15.706 4.55364C15.5704 4.70617 15.3835 4.80362 15.1808 4.8275L15.0833 4.83333H14.3791L13.3533 15.2667C13.2974 15.8328 13.033 16.3579 12.6114 16.7399C12.1899 17.1219 11.6413 17.3334 11.0724 17.3333H4.92742C4.35854 17.3334 3.80998 17.1219 3.38842 16.7399C2.96686 16.3579 2.70244 15.8328 2.64659 15.2667L1.62075 4.83333H0.916585C0.695572 4.83333 0.48361 4.74554 0.32733 4.58926C0.171049 4.43298 0.083252 4.22101 0.083252 4C0.083252 3.77899 0.171049 3.56702 0.32733 3.41074C0.48361 3.25446 0.695572 3.16667 0.916585 3.16667H5.08325C5.08325 2.78364 5.15869 2.40437 5.30527 2.05051C5.45185 1.69664 5.66669 1.37511 5.93752 1.10427C6.20836 0.833434 6.52989 0.618594 6.88376 0.472018C7.23763 0.325442 7.6169 0.25 7.99992 0.25C8.38294 0.25 8.76221 0.325442 9.11608 0.472018C9.46994 0.618594 9.79148 0.833434 10.0623 1.10427C10.3332 1.37511 10.548 1.69664 10.6946 2.05051C10.8411 2.40437 10.9166 2.78364 10.9166 3.16667H15.0833C15.3043 3.16667 15.5162 3.25446 15.6725 3.41074C15.8288 3.56702 15.9166 3.77899 15.9166 4ZM9.87492 6.70833C9.72389 6.70834 9.57797 6.76304 9.46414 6.86231C9.35032 6.96158 9.27629 7.09871 9.25575 7.24833L9.24992 7.33333V13.1667L9.25575 13.2517C9.27633 13.4013 9.35038 13.5383 9.4642 13.6376C9.57802 13.7368 9.72392 13.7915 9.87492 13.7915C10.0259 13.7915 10.1718 13.7368 10.2856 13.6376C10.3995 13.5383 10.4735 13.4013 10.4941 13.2517L10.4999 13.1667V7.33333L10.4941 7.24833C10.4735 7.09871 10.3995 6.96158 10.2857 6.86231C10.1719 6.76304 10.0259 6.70834 9.87492 6.70833ZM6.12492 6.70833C5.97389 6.70834 5.82797 6.76304 5.71414 6.86231C5.60032 6.96158 5.52629 7.09871 5.50575 7.24833L5.49992 7.33333V13.1667L5.50575 13.2517C5.52633 13.4013 5.60038 13.5383 5.7142 13.6376C5.82802 13.7368 5.97392 13.7915 6.12492 13.7915C6.27592 13.7915 6.42182 13.7368 6.53564 13.6376C6.64946 13.5383 6.7235 13.4013 6.74409 13.2517L6.74992 13.1667V7.33333L6.74409 7.24833C6.72355 7.09871 6.64952 6.96158 6.53569 6.86231C6.42187 6.76304 6.27595 6.70834 6.12492 6.70833ZM7.99992 1.91667C7.6684 1.91667 7.35046 2.04836 7.11603 2.28278C6.88161 2.5172 6.74992 2.83515 6.74992 3.16667H9.24992C9.24992 2.83515 9.11822 2.5172 8.8838 2.28278C8.64938 2.04836 8.33144 1.91667 7.99992 1.91667Z" fill="#8F9BB2"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -30,10 +30,11 @@ const useTabsStyles = makeStyles((theme : Theme) => createStyles({
|
|||||||
},
|
},
|
||||||
|
|
||||||
tab: {
|
tab: {
|
||||||
display: 'inline-block',
|
display: 'inline-flex',
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
color: variables.blueColor,
|
color: variables.blueColor,
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
|
alignItems: "center"
|
||||||
},
|
},
|
||||||
|
|
||||||
tabsAlignLeft: {
|
tabsAlignLeft: {
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
@import '../ServiceMapModal/ServiceMapModal.module'
|
||||||
|
@import '../../../variables.module'
|
||||||
|
|
||||||
|
.modalContainer
|
||||||
|
flex-direction: column
|
||||||
|
margin: 0 $modalMargin-from-edge
|
||||||
|
padding: 0
|
||||||
|
overflow-y: auto
|
||||||
|
|
||||||
|
.keyValueContainer
|
||||||
|
background-color: $content-section-color
|
||||||
|
height: 30%
|
||||||
|
border-radius: 5px
|
||||||
|
|
||||||
|
.sectionHeader
|
||||||
|
font-weight: 600
|
||||||
|
font-size: 1.2rem
|
||||||
|
|
||||||
|
.path
|
||||||
|
display: flex
|
||||||
|
|
||||||
|
input
|
||||||
|
border-radius: 0 5px 5px 0
|
||||||
|
flex: 1
|
||||||
|
font-size: 15px
|
||||||
|
color: unset
|
||||||
|
border-left-width: 0px
|
||||||
|
text-indent: 5px
|
||||||
|
|
||||||
|
.hostPort
|
||||||
|
border-radius : 0
|
||||||
|
border-width: 1px 1px 1px 0px
|
||||||
|
|
||||||
|
select
|
||||||
|
border-radius: 5px 0 0 5px
|
||||||
|
text-transform: uppercase
|
||||||
|
flex: 0 0 100px
|
||||||
|
text-align: center
|
||||||
|
font-size: 15px
|
||||||
|
font-weight: 600
|
||||||
|
.tabs
|
||||||
|
margin-top: 25px
|
||||||
|
|
||||||
|
.tabContent
|
||||||
|
height: 30%
|
||||||
|
border-radius: 5px
|
||||||
|
margin-top: 15px
|
||||||
|
|
||||||
|
.codeEditor
|
||||||
|
width: 100%
|
||||||
|
position: relative
|
||||||
|
height: 300px
|
||||||
|
border-radius: inherit
|
||||||
|
max-height: 40vh
|
||||||
|
min-height: 50px
|
||||||
|
|
||||||
|
.executeButton
|
||||||
|
text-transform: uppercase
|
||||||
|
width: fit-content
|
||||||
|
margin-left: 10px
|
||||||
|
|
||||||
|
.responseContainer
|
||||||
|
height: 80%
|
||||||
|
display: flex
|
||||||
|
justify-content: center
|
||||||
|
align-items: center
|
||||||
|
|
||||||
|
.note
|
||||||
|
color: $data-background-color
|
||||||
|
padding: 10px
|
||||||
|
margin-top: 10px
|
||||||
|
box-sizing: border-box
|
||||||
|
display: flex
|
||||||
|
font-style: italic
|
||||||
|
font-weight: 300
|
||||||
|
background-color: $light-gray
|
||||||
|
border-left: solid 4px $failure-color
|
||||||
|
line-height: 18px
|
||||||
|
overflow: hidden
|
||||||
|
|
||||||
|
b::after
|
||||||
|
content: '\b'
|
||||||
|
display: inline
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
import { Accordion, AccordionDetails, AccordionSummary, Backdrop, Box, Button, Fade, Modal } from "@mui/material";
|
||||||
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||||
|
import React, { Fragment, useCallback, useEffect, useState } from "react";
|
||||||
|
import { useCommonStyles } from "../../../helpers/commonStyle";
|
||||||
|
import { Tabs } from "../../UI";
|
||||||
|
import KeyValueTable from "../../UI/KeyValueTable/KeyValueTable";
|
||||||
|
import CodeEditor from "../../UI/CodeEditor/CodeEditor";
|
||||||
|
import { useRecoilValue, RecoilState, useRecoilState } from "recoil";
|
||||||
|
import TrafficViewerApiAtom from "../../../recoil/TrafficViewerApi/atom";
|
||||||
|
import TrafficViewerApi from "../../TrafficViewer/TrafficViewerApi";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { TOAST_CONTAINER_ID } from "../../../configs/Consts";
|
||||||
|
import styles from './ReplayRequestModal.module.sass'
|
||||||
|
import closeIcon from "assets/close.svg"
|
||||||
|
import spinnerImg from "assets/spinner.svg"
|
||||||
|
import refreshImg from "assets/refresh.svg"
|
||||||
|
import { formatRequest } from "../../EntryDetailed/EntrySections/EntrySections";
|
||||||
|
import entryDataAtom from "../../../recoil/entryData";
|
||||||
|
import { AutoRepresentation } from "../../EntryDetailed/EntryViewer/AutoRepresentation";
|
||||||
|
import useDebounce from "../../../hooks/useDebounce"
|
||||||
|
import replayRequestModalOpenAtom from "../../../recoil/replayRequestModalOpen";
|
||||||
|
import { Utils } from "../../../helpers/Utils";
|
||||||
|
|
||||||
|
const modalStyle = {
|
||||||
|
position: 'absolute',
|
||||||
|
top: '6%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, 0%)',
|
||||||
|
width: '89vw',
|
||||||
|
height: '82vh',
|
||||||
|
bgcolor: '#F0F5FF',
|
||||||
|
borderRadius: '5px',
|
||||||
|
boxShadow: 24,
|
||||||
|
p: 4,
|
||||||
|
color: '#000',
|
||||||
|
padding: "1px 1px",
|
||||||
|
paddingBottom: "15px"
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ReplayRequestModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RequestTabs {
|
||||||
|
Params = "params",
|
||||||
|
Headers = "headers",
|
||||||
|
Body = "body"
|
||||||
|
}
|
||||||
|
|
||||||
|
const HTTP_METHODS = ["get", "post", "put", "head", "options", "delete"]
|
||||||
|
const TABS = [{ tab: RequestTabs.Headers }, { tab: RequestTabs.Params }, { tab: RequestTabs.Body }];
|
||||||
|
|
||||||
|
const convertParamsToArr = (paramsObj) => Object.entries(paramsObj).map(([key, value]) => { return { key, value } })
|
||||||
|
|
||||||
|
const getQueryStringParams = (link: String) => {
|
||||||
|
|
||||||
|
if (link) {
|
||||||
|
const decodedURL = decodeQueryParam(link)
|
||||||
|
const query = decodedURL.split('?')[1]
|
||||||
|
const urlSearchParams = new URLSearchParams(query);
|
||||||
|
return Object.fromEntries(urlSearchParams.entries());
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
};
|
||||||
|
|
||||||
|
const decodeQueryParam = (p) => {
|
||||||
|
return decodeURIComponent(p.replace(/\+/g, ' '));
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReplayRequestModal: React.FC<ReplayRequestModalProps> = ({ isOpen, onClose }) => {
|
||||||
|
const entryData = useRecoilValue(entryDataAtom)
|
||||||
|
const request = entryData.data.request
|
||||||
|
const [method, setMethod] = useState(request?.method?.toLowerCase() as string)
|
||||||
|
const getHostUrl = useCallback(() => {
|
||||||
|
return entryData.data.dst.name ? entryData.data?.dst?.name : entryData.data.dst.ip
|
||||||
|
}, [entryData.data.dst.ip, entryData.data.dst.name])
|
||||||
|
const [hostPortInput, setHostPortInput] = useState(`${entryData.base.proto.name}://${getHostUrl()}:${entryData.data.dst.port}`)
|
||||||
|
const [pathInput, setPathInput] = useState(request.path);
|
||||||
|
const commonClasses = useCommonStyles();
|
||||||
|
const [currentTab, setCurrentTab] = useState(TABS[0].tab);
|
||||||
|
const [response, setResponse] = useState(null);
|
||||||
|
const [postData, setPostData] = useState(request?.postData?.text || JSON.stringify(request?.postData?.params));
|
||||||
|
const [params, setParams] = useState(convertParamsToArr(request?.queryString || {}))
|
||||||
|
const [headers, setHeaders] = useState(convertParamsToArr(request?.headers || {}))
|
||||||
|
const trafficViewerApi = useRecoilValue(TrafficViewerApiAtom as RecoilState<TrafficViewerApi>)
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [requestExpanded, setRequestExpanded] = useState(true)
|
||||||
|
const [responseExpanded, setResponseExpanded] = useState(false)
|
||||||
|
|
||||||
|
const debouncedPath = useDebounce(pathInput, 500);
|
||||||
|
|
||||||
|
const onParamsChange = useCallback((newParams) => {
|
||||||
|
setParams(newParams);
|
||||||
|
let newUrl = `${debouncedPath ? debouncedPath.split('?')[0] : ""}`
|
||||||
|
newParams.forEach(({ key, value }, index) => {
|
||||||
|
newUrl += index > 0 ? '&' : '?'
|
||||||
|
newUrl += `${key}` + (value ? `=${value}` : "")
|
||||||
|
})
|
||||||
|
|
||||||
|
setPathInput(newUrl)
|
||||||
|
|
||||||
|
}, [debouncedPath])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const newParams = getQueryStringParams(debouncedPath);
|
||||||
|
setParams(convertParamsToArr(newParams))
|
||||||
|
}, [debouncedPath])
|
||||||
|
|
||||||
|
const onModalClose = () => {
|
||||||
|
setRequestExpanded(true)
|
||||||
|
setResponseExpanded(true)
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetModel = useCallback(() => {
|
||||||
|
setMethod(request?.method?.toLowerCase() as string)
|
||||||
|
setHostPortInput(`${entryData.base.proto.name}://${getHostUrl()}:${entryData.data.dst.port}`)
|
||||||
|
setPathInput(request.path);
|
||||||
|
setResponse(null);
|
||||||
|
setPostData(request?.postData?.text || JSON.stringify(request?.postData?.params));
|
||||||
|
setParams(convertParamsToArr(request?.queryString || {}))
|
||||||
|
setHeaders(convertParamsToArr(request?.headers || {}))
|
||||||
|
setRequestExpanded(true)
|
||||||
|
}, [entryData.base.proto.name, entryData.data.dst.port, getHostUrl, request?.headers, request?.method, request.path, request?.postData?.params, request?.postData?.text, request?.queryString])
|
||||||
|
|
||||||
|
const onRefreshRequest = useCallback((event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
resetModel()
|
||||||
|
}, [resetModel])
|
||||||
|
|
||||||
|
|
||||||
|
const sendRequest = useCallback(async () => {
|
||||||
|
setResponse(null)
|
||||||
|
const headersData = headers.reduce((prev, corrent) => {
|
||||||
|
prev[corrent.key] = corrent.value
|
||||||
|
return prev
|
||||||
|
}, {})
|
||||||
|
const buildUrl = `${hostPortInput}${pathInput}`
|
||||||
|
const requestData = { url: buildUrl, headers: headersData, data: postData, method }
|
||||||
|
try {
|
||||||
|
setIsLoading(true)
|
||||||
|
const response = await trafficViewerApi.replayRequest(requestData)
|
||||||
|
setResponse(response?.data?.representation)
|
||||||
|
if (response.errorMessage) {
|
||||||
|
toast.error(response.errorMessage, { containerId: TOAST_CONTAINER_ID });
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setRequestExpanded(false)
|
||||||
|
setResponseExpanded(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
setRequestExpanded(true)
|
||||||
|
toast.error("Error occurred while fetching response", { containerId: TOAST_CONTAINER_ID });
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [headers, hostPortInput, method, pathInput, postData, trafficViewerApi])
|
||||||
|
|
||||||
|
let innerComponent
|
||||||
|
switch (currentTab) {
|
||||||
|
case RequestTabs.Params:
|
||||||
|
innerComponent = <div className={styles.keyValueContainer}><KeyValueTable data={params} onDataChange={onParamsChange} key={"params"} valuePlaceholder="New Param Value" keyPlaceholder="New param Key" /></div>
|
||||||
|
break;
|
||||||
|
case RequestTabs.Headers:
|
||||||
|
innerComponent = <Fragment>
|
||||||
|
<div className={styles.keyValueContainer}><KeyValueTable data={headers} onDataChange={(heaedrs) => setHeaders(heaedrs)} key={"Header"} valuePlaceholder="New Headers Value" keyPlaceholder="New Headers Key" />
|
||||||
|
</div>
|
||||||
|
<span className={styles.note}><b>* </b> X-Mizu Header added to reuqests</span>
|
||||||
|
</Fragment>
|
||||||
|
break;
|
||||||
|
case RequestTabs.Body:
|
||||||
|
const formatedCode = formatRequest(postData || "", request?.postData?.mimeType)
|
||||||
|
innerComponent = <div className={styles.codeEditor}>
|
||||||
|
<CodeEditor language={request?.postData?.mimeType.split("/")[1]}
|
||||||
|
code={Utils.isJson(formatedCode) ? JSON.stringify(JSON.parse(formatedCode || "{}"), null, 2) : formatedCode}
|
||||||
|
onChange={setPostData} />
|
||||||
|
</div>
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
innerComponent = null
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
aria-labelledby="transition-modal-title"
|
||||||
|
aria-describedby="transition-modal-description"
|
||||||
|
open={isOpen}
|
||||||
|
onClose={onModalClose}
|
||||||
|
closeAfterTransition
|
||||||
|
BackdropComponent={Backdrop}
|
||||||
|
BackdropProps={{ timeout: 500 }}>
|
||||||
|
<Fade in={isOpen}>
|
||||||
|
<Box sx={modalStyle}>
|
||||||
|
<div className={styles.closeIcon}>
|
||||||
|
<img src={closeIcon} alt="close" onClick={onModalClose} style={{ cursor: "pointer", userSelect: "none" }} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.headerContainer}>
|
||||||
|
<div className={styles.headerSection}>
|
||||||
|
<span className={styles.title}>Replay Request</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.modalContainer}>
|
||||||
|
<Accordion TransitionProps={{ unmountOnExit: true }} expanded={requestExpanded} onChange={() => setRequestExpanded(!requestExpanded)}>
|
||||||
|
<AccordionSummary expandIcon={<ExpandMoreIcon />} aria-controls="response-content">
|
||||||
|
<span className={styles.sectionHeader}>REQUEST</span>
|
||||||
|
<img src={refreshImg} style={{ marginLeft: "10px" }} title="Refresh Reuqest" alt="Refresh Reuqest" onClick={onRefreshRequest} />
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<div className={styles.path}>
|
||||||
|
<select className={styles.select} value={method} onChange={(e) => setMethod(e.target.value)}>
|
||||||
|
{HTTP_METHODS.map(method => <option value={method} key={method}>{method}</option>)}
|
||||||
|
</select>
|
||||||
|
<input placeholder="Host:Port" value={hostPortInput} onChange={(event) => setHostPortInput(event.target.value)} className={`${commonClasses.textField} ${styles.hostPort}`} />
|
||||||
|
<input className={commonClasses.textField} placeholder="Enter Path" value={pathInput}
|
||||||
|
onChange={(event) => setPathInput(event.target.value)} />
|
||||||
|
<Button size="medium"
|
||||||
|
variant="contained"
|
||||||
|
className={commonClasses.button + ` ${styles.executeButton}`}
|
||||||
|
onClick={sendRequest}>
|
||||||
|
Execute
|
||||||
|
</Button >
|
||||||
|
</div>
|
||||||
|
<Tabs tabs={TABS} currentTab={currentTab} onChange={setCurrentTab} leftAligned classes={{ root: styles.tabs }} />
|
||||||
|
<div className={styles.tabContent}>
|
||||||
|
{innerComponent}
|
||||||
|
</div>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
{isLoading && <img alt="spinner" src={spinnerImg} style={{ height: 50 }} />}
|
||||||
|
{response && !isLoading && (<Accordion TransitionProps={{ unmountOnExit: true }} expanded={responseExpanded} onChange={() => setResponseExpanded(!responseExpanded)}>
|
||||||
|
<AccordionSummary expandIcon={<ExpandMoreIcon />} aria-controls="response-content">
|
||||||
|
<span className={styles.sectionHeader}>RESPONSE</span>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<AutoRepresentation representation={response} color={entryData.protocol.backgroundColor} />
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>)}
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
</Fade>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReplayRequestModalContainer = () => {
|
||||||
|
const [isOpenRequestModal, setIsOpenRequestModal] = useRecoilState(replayRequestModalOpenAtom)
|
||||||
|
return isOpenRequestModal && < ReplayRequestModal isOpen={isOpenRequestModal} onClose={() => setIsOpenRequestModal(false)} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReplayRequestModalContainer
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M18.591 9.99997C18.591 14.7446 14.7447 18.5909 10.0001 18.5909C5.25546 18.5909 1.40918 14.7446 1.40918 9.99997C1.40918 5.25534 5.25546 1.40906 10.0001 1.40906C14.7447 1.40906 18.591 5.25534 18.591 9.99997Z" fill="#E9EBF8" stroke="#BCCEFD"/>
|
||||||
|
<path d="M13.1604 8.23038L11.95 7.01994L10.1392 8.83078L8.32832 7.01994L7.11789 8.23038L8.92872 10.0412L7.12046 11.8495L8.33089 13.0599L10.1392 11.2517L11.9474 13.0599L13.1579 11.8495L11.3496 10.0412L13.1604 8.23038Z" fill="#205CF5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 588 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M10.8337 11.9167H7.69308L7.69416 11.907C7.83561 11.2143 8.11247 10.5564 8.50883 9.97105C9.09865 9.10202 9.92598 8.42097 10.8922 8.00913C11.2193 7.87046 11.5606 7.7643 11.9083 7.69388C12.6297 7.54762 13.3731 7.54762 14.0945 7.69388C15.1312 7.90631 16.0825 8.41908 16.8299 9.1683L18.3639 7.63863C17.6725 6.94707 16.8546 6.39501 15.9546 6.01255C15.4956 5.81823 15.0184 5.67016 14.53 5.57055C13.5223 5.36581 12.4838 5.36581 11.4761 5.57055C10.9873 5.67057 10.5098 5.819 10.0504 6.01363C8.69682 6.58791 7.53808 7.54123 6.71374 8.7588C6.15895 9.5798 5.77099 10.5019 5.57191 11.4725C5.54158 11.6188 5.52533 11.7683 5.50366 11.9167H2.16699L6.50033 16.25L10.8337 11.9167ZM15.167 14.0834H18.3076L18.3065 14.092C18.0234 15.4806 17.205 16.7019 16.0282 17.4915C15.443 17.8882 14.7851 18.1651 14.0923 18.3062C13.3713 18.4525 12.6283 18.4525 11.9072 18.3062C11.2146 18.1648 10.5567 17.8879 9.97133 17.4915C9.68383 17.2971 9.41541 17.0758 9.16966 16.8307L7.63783 18.3625C8.32954 19.0539 9.14791 19.6056 10.0482 19.9875C10.5076 20.1825 10.9875 20.331 11.4728 20.4295C12.4801 20.6344 13.5184 20.6344 14.5257 20.4295C16.4676 20.0265 18.1757 18.8819 19.2869 17.2391C19.8412 16.4187 20.2288 15.4974 20.4277 14.5275C20.4569 14.3813 20.4742 14.2318 20.4959 14.0834H23.8337L19.5003 9.75005L15.167 14.0834Z" fill="#205CF5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin: auto; background: none; display: block; shape-rendering: auto;" width="200px" height="200px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
|
||||||
|
<circle cx="50" cy="50" fill="none" stroke="#1d3f72" stroke-width="10" r="35" stroke-dasharray="164.93361431346415 56.97787143782138" transform="rotate(275.903 50 50)">
|
||||||
|
<animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="1s" values="0 50 50;360 50 50" keyTimes="0;1"></animateTransform>
|
||||||
|
</circle>
|
||||||
|
<!-- [ldio] generated by https://loading.io/ --></svg>
|
||||||
|
After Width: | Height: | Size: 673 B |
@@ -1,6 +1,8 @@
|
|||||||
@import "../../../variables.module"
|
@import "../../../variables.module"
|
||||||
@import "../../../components"
|
@import "../../../components"
|
||||||
|
|
||||||
|
$modalMargin-from-edge : 35px
|
||||||
|
|
||||||
.closeIcon
|
.closeIcon
|
||||||
position: absolute
|
position: absolute
|
||||||
right: 20px
|
right: 20px
|
||||||
@@ -24,7 +26,7 @@
|
|||||||
display: flex
|
display: flex
|
||||||
align-content: center
|
align-content: center
|
||||||
align-items: center
|
align-items: center
|
||||||
margin-left: 35px
|
margin-left: $modalMargin-from-edge
|
||||||
margin-bottom: 25px
|
margin-bottom: 25px
|
||||||
margin-top: 25px
|
margin-top: 25px
|
||||||
|
|
||||||
|
|||||||
@@ -1,70 +1,79 @@
|
|||||||
import styles from "./TimelineBarChart.module.sass";
|
import styles from "./TimelineBarChart.module.sass";
|
||||||
import { StatsMode } from "../TrafficStatsModal"
|
import { ALL_PROTOCOLS, StatsMode } from "../TrafficStatsModal"
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
BarChart,
|
BarChart,
|
||||||
Bar,
|
Bar,
|
||||||
XAxis,
|
XAxis,
|
||||||
YAxis,
|
YAxis,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Legend
|
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
import { Utils } from "../../../../helpers/Utils";
|
import { Utils } from "../../../../helpers/Utils";
|
||||||
|
|
||||||
interface TimelineBarChartProps {
|
interface TimelineBarChartProps {
|
||||||
timeLineBarChartMode: string;
|
timeLineBarChartMode: string;
|
||||||
data: any;
|
data: any;
|
||||||
|
selectedProtocol: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TimelineBarChart: React.FC<TimelineBarChartProps> = ({ timeLineBarChartMode, data }) => {
|
export const TimelineBarChart: React.FC<TimelineBarChartProps> = ({ timeLineBarChartMode, data, selectedProtocol }) => {
|
||||||
const [protocolStats, setProtocolStats] = useState([]);
|
const [protocolStats, setProtocolStats] = useState([]);
|
||||||
const [protocolsNamesAndColors, setProtocolsNamesAndColors] = useState([]);
|
const [protocolsNamesAndColors, setProtocolsNamesAndColors] = useState([]);
|
||||||
|
const [commandStats, setCommandStats] = useState(null);
|
||||||
const padTo2Digits = useCallback((num) => {
|
const [commandNames, setcommandNames] = useState(null);
|
||||||
return String(num).padStart(2, '0');
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const getHoursAndMinutes = useCallback((protocolTimeKey) => {
|
|
||||||
const time = new Date(protocolTimeKey)
|
|
||||||
const hoursAndMinutes = padTo2Digits(time.getHours()) + ':' + padTo2Digits(time.getMinutes());
|
|
||||||
return hoursAndMinutes;
|
|
||||||
}, [padTo2Digits])
|
|
||||||
|
|
||||||
const creatUniqueObjArray = useCallback((objArray) => {
|
|
||||||
return [
|
|
||||||
...new Map(objArray.map((item) => [item["name"], item])).values(),
|
|
||||||
];
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
const protocolsBarsData = [];
|
const protocolsBarsData = [];
|
||||||
const prtcNames = [];
|
const prtcNames = [];
|
||||||
data.forEach(protocolObj => {
|
data.forEach(protocolObj => {
|
||||||
let obj: { [k: string]: any } = {};
|
let newProtocolbj: { [k: string]: any } = {};
|
||||||
obj.timestamp = getHoursAndMinutes(protocolObj.timestamp);
|
newProtocolbj.timestamp = Utils.getHoursAndMinutes(protocolObj.timestamp);
|
||||||
protocolObj.protocols.forEach(protocol => {
|
protocolObj.protocols.forEach(protocol => {
|
||||||
obj[`${protocol.name}`] = protocol[StatsMode[timeLineBarChartMode]];
|
newProtocolbj[`${protocol.name}`] = protocol[StatsMode[timeLineBarChartMode]];
|
||||||
prtcNames.push({ name: protocol.name, color: protocol.color });
|
prtcNames.push({ name: protocol.name, color: protocol.color });
|
||||||
})
|
})
|
||||||
protocolsBarsData.push(obj);
|
protocolsBarsData.push(newProtocolbj);
|
||||||
})
|
})
|
||||||
const uniqueObjArray = creatUniqueObjArray(prtcNames);
|
const uniqueObjArray = Utils.creatUniqueObjArrayByProp(prtcNames, "name")
|
||||||
protocolsBarsData.sort((a, b) => a.timestamp < b.timestamp ? -1 : 1);
|
protocolsBarsData.sort((a, b) => a.timestamp < b.timestamp ? -1 : 1);
|
||||||
setProtocolStats(protocolsBarsData);
|
setProtocolStats(protocolsBarsData);
|
||||||
setProtocolsNamesAndColors(uniqueObjArray);
|
setProtocolsNamesAndColors(uniqueObjArray);
|
||||||
}, [data, timeLineBarChartMode, setProtocolStats, setProtocolsNamesAndColors, creatUniqueObjArray, getHoursAndMinutes])
|
}, [data, timeLineBarChartMode])
|
||||||
|
|
||||||
const bars = useMemo(() => protocolsNamesAndColors.map((protocolToDIsplay) => {
|
useEffect(() => {
|
||||||
return <Bar key={protocolToDIsplay.name} dataKey={protocolToDIsplay.name} stackId="a" fill={protocolToDIsplay.color} />
|
if (selectedProtocol === ALL_PROTOCOLS) {
|
||||||
}), [protocolsNamesAndColors])
|
setCommandStats(null);
|
||||||
|
setcommandNames(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const commandsNames = [];
|
||||||
|
const protocolsCommands = [];
|
||||||
|
data.forEach(protocolObj => {
|
||||||
|
let newCommandlbj: { [k: string]: any } = {};
|
||||||
|
newCommandlbj.timestamp = Utils.getHoursAndMinutes(protocolObj.timestamp);
|
||||||
|
protocolObj.protocols.find(protocol => protocol.name === selectedProtocol)?.methods.forEach(command => {
|
||||||
|
newCommandlbj[`${command.name}`] = command[StatsMode[timeLineBarChartMode]]
|
||||||
|
if (commandsNames.indexOf(command.name) === -1)
|
||||||
|
commandsNames.push(command.name);
|
||||||
|
})
|
||||||
|
protocolsCommands.push(newCommandlbj);
|
||||||
|
})
|
||||||
|
protocolsCommands.sort((a, b) => a.timestamp < b.timestamp ? -1 : 1);
|
||||||
|
setcommandNames(commandsNames);
|
||||||
|
setCommandStats(protocolsCommands);
|
||||||
|
}, [data, timeLineBarChartMode, selectedProtocol])
|
||||||
|
|
||||||
|
const bars = useMemo(() => (commandNames || protocolsNamesAndColors).map((entry) => {
|
||||||
|
return <Bar key={entry.name || entry} dataKey={entry.name || entry} stackId="a" fill={entry.color || Utils.stringToColor(entry)} />
|
||||||
|
}), [protocolsNamesAndColors, commandNames])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.barChartContainer}>
|
<div className={styles.barChartContainer}>
|
||||||
<BarChart
|
{protocolStats.length > 0 && <BarChart
|
||||||
width={730}
|
width={730}
|
||||||
height={250}
|
height={250}
|
||||||
data={protocolStats}
|
data={commandStats || protocolStats}
|
||||||
margin={{
|
margin={{
|
||||||
top: 20,
|
top: 20,
|
||||||
right: 30,
|
right: 30,
|
||||||
@@ -75,9 +84,8 @@ export const TimelineBarChart: React.FC<TimelineBarChartProps> = ({ timeLineBarC
|
|||||||
<XAxis dataKey="timestamp" />
|
<XAxis dataKey="timestamp" />
|
||||||
<YAxis tickFormatter={(value) => timeLineBarChartMode === "VOLUME" ? Utils.humanFileSize(value) : value} />
|
<YAxis tickFormatter={(value) => timeLineBarChartMode === "VOLUME" ? Utils.humanFileSize(value) : value} />
|
||||||
<Tooltip formatter={(value) => timeLineBarChartMode === "VOLUME" ? Utils.humanFileSize(value) : value + " Requests"} />
|
<Tooltip formatter={(value) => timeLineBarChartMode === "VOLUME" ? Utils.humanFileSize(value) : value + " Requests"} />
|
||||||
<Legend />
|
|
||||||
{bars}
|
{bars}
|
||||||
</BarChart>
|
</BarChart>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
.breadCrumbsContainer
|
|
||||||
margin-top: 15px
|
|
||||||
height: 15px
|
|
||||||
|
|
||||||
.breadCrumbs
|
|
||||||
color: #494677
|
|
||||||
text-align: left
|
|
||||||
|
|
||||||
.clickableTag
|
|
||||||
margin-right: 5px
|
|
||||||
border-bottom: 1px black solid
|
|
||||||
cursor: pointer
|
|
||||||
|
|
||||||
.nonClickableTag
|
|
||||||
margin-left: 5px
|
|
||||||
font-weight: 600
|
|
||||||
@@ -1,21 +1,18 @@
|
|||||||
import React, {useEffect, useMemo, useState} from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import styles from "./TrafficPieChart.module.sass";
|
import { Cell, Legend, Pie, PieChart, Tooltip } from "recharts";
|
||||||
import {Cell, Legend, Pie, PieChart, Tooltip} from "recharts";
|
import { Utils } from "../../../../helpers/Utils";
|
||||||
import {Utils} from "../../../../helpers/Utils";
|
import { ALL_PROTOCOLS, StatsMode as PieChartMode } from "../TrafficStatsModal"
|
||||||
import {StatsMode as PieChartMode} from "../TrafficStatsModal"
|
|
||||||
|
|
||||||
const COLORS = ['#e6194b', '#3cb44b', '#ffe119', '#4363d8', '#f58231', '#911eb4', '#46f0f0', '#f032e6', '#bcf60c', '#fabebe', '#008080', '#e6beff', '#9a6324', '#fffac8', '#800000', '#aaffc3', '#808000', '#ffd8b1', '#000075', '#808080', '#ffffff', '#000000'];
|
|
||||||
|
|
||||||
const RADIAN = Math.PI / 180;
|
const RADIAN = Math.PI / 180;
|
||||||
const renderCustomizedLabel = ({
|
const renderCustomizedLabel = ({
|
||||||
cx,
|
cx,
|
||||||
cy,
|
cy,
|
||||||
midAngle,
|
midAngle,
|
||||||
innerRadius,
|
innerRadius,
|
||||||
outerRadius,
|
outerRadius,
|
||||||
percent,
|
percent,
|
||||||
index
|
index
|
||||||
}: any) => {
|
}: any) => {
|
||||||
const radius = innerRadius + (outerRadius - innerRadius) * 0.5;
|
const radius = innerRadius + (outerRadius - innerRadius) * 0.5;
|
||||||
const x = cx + radius * Math.cos(-midAngle * RADIAN);
|
const x = cx + radius * Math.cos(-midAngle * RADIAN);
|
||||||
const y = cy + radius * Math.sin(-midAngle * RADIAN);
|
const y = cy + radius * Math.sin(-midAngle * RADIAN);
|
||||||
@@ -38,13 +35,13 @@ const renderCustomizedLabel = ({
|
|||||||
interface TrafficPieChartProps {
|
interface TrafficPieChartProps {
|
||||||
pieChartMode: string;
|
pieChartMode: string;
|
||||||
data: any;
|
data: any;
|
||||||
|
selectedProtocol: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TrafficPieChart: React.FC<TrafficPieChartProps> = ({pieChartMode , data}) => {
|
export const TrafficPieChart: React.FC<TrafficPieChartProps> = ({ pieChartMode, data, selectedProtocol }) => {
|
||||||
|
|
||||||
const [protocolsStats, setProtocolsStats] = useState([]);
|
const [protocolsStats, setProtocolsStats] = useState([]);
|
||||||
const [commandStats, setCommandStats] = useState(null);
|
const [commandStats, setCommandStats] = useState(null);
|
||||||
const [selectedProtocol, setSelectedProtocol] = useState(null as string);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
@@ -59,11 +56,11 @@ export const TrafficPieChart: React.FC<TrafficPieChartProps> = ({pieChartMode ,
|
|||||||
}, [data, pieChartMode])
|
}, [data, pieChartMode])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedProtocol) {
|
if (selectedProtocol === ALL_PROTOCOLS) {
|
||||||
setCommandStats(null);
|
setCommandStats(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const commandsPieData = data.find(protocol => protocol.name === selectedProtocol).methods.map(command => {
|
const commandsPieData = data.find(protocol => protocol.name === selectedProtocol)?.methods.map(command => {
|
||||||
return {
|
return {
|
||||||
name: command.name,
|
name: command.name,
|
||||||
value: command[PieChartMode[pieChartMode]]
|
value: command[PieChartMode[pieChartMode]]
|
||||||
@@ -75,18 +72,18 @@ export const TrafficPieChart: React.FC<TrafficPieChartProps> = ({pieChartMode ,
|
|||||||
const pieLegend = useMemo(() => {
|
const pieLegend = useMemo(() => {
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
let legend;
|
let legend;
|
||||||
if (!selectedProtocol) {
|
if (selectedProtocol === ALL_PROTOCOLS) {
|
||||||
legend = data.map(protocol => <div style={{marginBottom: 5, display: "flex"}}>
|
legend = data.map(protocol => <div style={{ marginBottom: 5, display: "flex" }}>
|
||||||
<div style={{height: 15, width: 30, background: protocol?.color}}/>
|
<div style={{ height: 15, width: 30, background: protocol?.color }} />
|
||||||
<span style={{marginLeft: 5}}>
|
<span style={{ marginLeft: 5 }}>
|
||||||
{protocol.name}
|
{protocol.name}
|
||||||
</span>
|
</span>
|
||||||
</div>)
|
</div>)
|
||||||
} else {
|
} else {
|
||||||
legend = data.find(protocol => protocol.name === selectedProtocol).methods.map((method, index) => <div
|
legend = data.find(protocol => protocol.name === selectedProtocol)?.methods.map((method) => <div
|
||||||
style={{marginBottom: 5, display: "flex"}}>
|
style={{ marginBottom: 5, display: "flex" }}>
|
||||||
<div style={{height: 15, width: 30, background: COLORS[index % COLORS.length]}}/>
|
<div style={{ height: 15, width: 30, background: Utils.stringToColor(method.name)}} />
|
||||||
<span style={{marginLeft: 5}}>
|
<span style={{ marginLeft: 5 }}>
|
||||||
{method.name}
|
{method.name}
|
||||||
</span>
|
</span>
|
||||||
</div>)
|
</div>)
|
||||||
@@ -96,15 +93,7 @@ export const TrafficPieChart: React.FC<TrafficPieChartProps> = ({pieChartMode ,
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={styles.breadCrumbsContainer}>
|
{protocolsStats?.length > 0 && <div style={{ width: "100%", display: "flex", justifyContent: "center" }}>
|
||||||
{selectedProtocol && <div className={styles.breadCrumbs}>
|
|
||||||
<span className={styles.clickableTag} onClick={() => setSelectedProtocol(null)}>protocols</span>
|
|
||||||
<span>/</span>
|
|
||||||
<span className={styles.nonClickableTag}>{selectedProtocol}</span>
|
|
||||||
</div>}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{protocolsStats?.length > 0 && <div style={{width: "100%", display: "flex", justifyContent: "center"}}>
|
|
||||||
<PieChart width={300} height={300}>
|
<PieChart width={300} height={300}>
|
||||||
<Pie
|
<Pie
|
||||||
data={commandStats || protocolsStats}
|
data={commandStats || protocolsStats}
|
||||||
@@ -114,14 +103,13 @@ export const TrafficPieChart: React.FC<TrafficPieChartProps> = ({pieChartMode ,
|
|||||||
labelLine={false}
|
labelLine={false}
|
||||||
label={renderCustomizedLabel}
|
label={renderCustomizedLabel}
|
||||||
outerRadius={125}
|
outerRadius={125}
|
||||||
fill="#8884d8"
|
fill="#8884d8">
|
||||||
onClick={(section) => !commandStats && setSelectedProtocol(section.name)}>
|
|
||||||
{(commandStats || protocolsStats).map((entry, index) => (
|
{(commandStats || protocolsStats).map((entry, index) => (
|
||||||
<Cell key={`cell-${index}`} fill={entry.color || COLORS[index % COLORS.length]}/>)
|
<Cell key={`cell-${index}`} fill={entry.color || Utils.stringToColor(entry.name)} />)
|
||||||
)}
|
)}
|
||||||
</Pie>
|
</Pie>
|
||||||
<Legend wrapperStyle={{position: "absolute", width: "auto", height: "auto", right: -150, top: 0}} content={pieLegend}/>
|
<Legend wrapperStyle={{ position: "absolute", width: "auto", height: "auto", right: -150, top: 0 }} content={pieLegend} />
|
||||||
<Tooltip formatter={(value) => pieChartMode === "VOLUME" ? Utils.humanFileSize(value) : value + " Requests"}/>
|
<Tooltip formatter={(value) => pieChartMode === "VOLUME" ? Utils.humanFileSize(value) : value + " Requests"} />
|
||||||
</PieChart>
|
</PieChart>
|
||||||
</div>}
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
.headlineContainer
|
||||||
|
display: flex
|
||||||
|
|
||||||
.title
|
.title
|
||||||
color: #494677
|
color: #494677
|
||||||
font-family: Source Sans Pro,Lucida Grande,Tahoma,sans-serif
|
font-family: Source Sans Pro,Lucida Grande,Tahoma,sans-serif
|
||||||
@@ -13,6 +16,11 @@
|
|||||||
padding: 30px
|
padding: 30px
|
||||||
text-align: center
|
text-align: center
|
||||||
|
|
||||||
|
.selectContainer
|
||||||
|
display: flex
|
||||||
|
justify-content: space-evenly
|
||||||
|
margin-bottom: 4%
|
||||||
|
|
||||||
.select
|
.select
|
||||||
border: none
|
border: none
|
||||||
border-bottom: 1px black solid
|
border-bottom: 1px black solid
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
import { Backdrop, Box, Fade, Modal } from "@mui/material";
|
import { Backdrop, Box, Button, debounce, Fade, Modal } from "@mui/material";
|
||||||
import styles from "./TrafficStatsModal.module.sass";
|
import styles from "./TrafficStatsModal.module.sass";
|
||||||
import closeIcon from "assets/close.svg";
|
import closeIcon from "assets/close.svg";
|
||||||
import { TrafficPieChart } from "./TrafficPieChart/TrafficPieChart";
|
import { TrafficPieChart } from "./TrafficPieChart/TrafficPieChart";
|
||||||
import { TimelineBarChart } from "./TimelineBarChart/TimelineBarChart";
|
import { TimelineBarChart } from "./TimelineBarChart/TimelineBarChart";
|
||||||
import spinnerImg from "assets/spinner.svg";
|
import spinnerImg from "assets/spinner.svg";
|
||||||
|
import refreshIcon from "assets/refresh.svg";
|
||||||
|
import { useCommonStyles } from "../../../helpers/commonStyle";
|
||||||
|
|
||||||
const modalStyle = {
|
const modalStyle = {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -32,15 +34,20 @@ interface TrafficStatsModalProps {
|
|||||||
getTimelineStatsDataApi: () => Promise<any>
|
getTimelineStatsDataApi: () => Promise<any>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const PROTOCOLS = ["ALL PROTOCOLS","gRPC", "REDIS", "HTTP", "GQL", "AMQP", "KFAKA"];
|
||||||
|
export const ALL_PROTOCOLS = PROTOCOLS[0];
|
||||||
|
|
||||||
export const TrafficStatsModal: React.FC<TrafficStatsModalProps> = ({ isOpen, onClose, getPieStatsDataApi, getTimelineStatsDataApi }) => {
|
export const TrafficStatsModal: React.FC<TrafficStatsModalProps> = ({ isOpen, onClose, getPieStatsDataApi, getTimelineStatsDataApi }) => {
|
||||||
|
|
||||||
const modes = Object.keys(StatsMode).filter(x => !(parseInt(x) >= 0));
|
const modes = Object.keys(StatsMode).filter(x => !(parseInt(x) >= 0));
|
||||||
const [statsMode, setStatsMode] = useState(modes[0]);
|
const [statsMode, setStatsMode] = useState(modes[0]);
|
||||||
|
const [selectedProtocol, setSelectedProtocol] = useState("ALL PROTOCOLS");
|
||||||
const [pieStatsData, setPieStatsData] = useState(null);
|
const [pieStatsData, setPieStatsData] = useState(null);
|
||||||
const [timelineStatsData, setTimelineStatsData] = useState(null);
|
const [timelineStatsData, setTimelineStatsData] = useState(null);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const commonClasses = useCommonStyles();
|
||||||
|
|
||||||
useEffect(() => {
|
const getTrafficStats = useCallback(async () => {
|
||||||
if (isOpen && getPieStatsDataApi) {
|
if (isOpen && getPieStatsDataApi) {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -58,6 +65,14 @@ export const TrafficStatsModal: React.FC<TrafficStatsModalProps> = ({ isOpen, on
|
|||||||
}
|
}
|
||||||
}, [isOpen, getPieStatsDataApi, getTimelineStatsDataApi, setPieStatsData, setTimelineStatsData])
|
}, [isOpen, getPieStatsDataApi, getTimelineStatsDataApi, setPieStatsData, setTimelineStatsData])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getTrafficStats();
|
||||||
|
}, [getTrafficStats])
|
||||||
|
|
||||||
|
const refreshStats = debounce(() => {
|
||||||
|
getTrafficStats();
|
||||||
|
}, 500);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
aria-labelledby="transition-modal-title"
|
aria-labelledby="transition-modal-title"
|
||||||
@@ -72,21 +87,40 @@ export const TrafficStatsModal: React.FC<TrafficStatsModalProps> = ({ isOpen, on
|
|||||||
<div className={styles.closeIcon}>
|
<div className={styles.closeIcon}>
|
||||||
<img src={closeIcon} alt="close" onClick={() => onClose()} style={{ cursor: "pointer", userSelect: "none" }} />
|
<img src={closeIcon} alt="close" onClick={() => onClose()} style={{ cursor: "pointer", userSelect: "none" }} />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.title}>Traffic Statistics</div>
|
<div className={styles.headlineContainer}>
|
||||||
|
<div className={styles.title}>Traffic Statistics</div>
|
||||||
|
<Button style={{ marginLeft: "2%", textTransform: 'unset' }}
|
||||||
|
startIcon={<img src={refreshIcon} className="custom" alt="refresh"></img>}
|
||||||
|
size="medium"
|
||||||
|
variant="contained"
|
||||||
|
className={commonClasses.outlinedButton + " " + commonClasses.imagedButton}
|
||||||
|
onClick={refreshStats}
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<div className={styles.mainContainer}>
|
<div className={styles.mainContainer}>
|
||||||
<div>
|
<div className={styles.selectContainer}>
|
||||||
<span style={{ marginRight: 15 }}>Breakdown By</span>
|
<div>
|
||||||
<select className={styles.select} value={statsMode} onChange={(e) => setStatsMode(e.target.value)}>
|
<span style={{ marginRight: 15 }}>Breakdown By</span>
|
||||||
{modes.map(mode => <option key={mode} value={mode}>{mode}</option>)}
|
<select className={styles.select} value={statsMode} onChange={(e) => setStatsMode(e.target.value)}>
|
||||||
</select>
|
{modes.map(mode => <option key={mode} value={mode}>{mode}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span style={{ marginRight: 15 }}>Protocol</span>
|
||||||
|
<select className={styles.select} value={selectedProtocol} onChange={(e) => setSelectedProtocol(e.target.value)}>
|
||||||
|
{PROTOCOLS.map(protocol => <option key={protocol} value={protocol}>{protocol}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{isLoading ? <div style={{ textAlign: "center", marginTop: 20 }}>
|
{isLoading ? <div style={{ textAlign: "center", marginTop: 20 }}>
|
||||||
<img alt="spinner" src={spinnerImg} style={{ height: 50 }} />
|
<img alt="spinner" src={spinnerImg} style={{ height: 50 }} />
|
||||||
</div> :
|
</div> :
|
||||||
<div>
|
<div>
|
||||||
<TrafficPieChart pieChartMode={statsMode} data={pieStatsData} />
|
<TrafficPieChart pieChartMode={statsMode} data={pieStatsData} selectedProtocol={selectedProtocol}/>
|
||||||
<TimelineBarChart timeLineBarChartMode={statsMode} data={timelineStatsData} />
|
<TimelineBarChart timeLineBarChartMode={statsMode} data={timelineStatsData} selectedProtocol={selectedProtocol}/>
|
||||||
</div>}
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M10.8337 11.9167H7.69308L7.69416 11.907C7.83561 11.2143 8.11247 10.5564 8.50883 9.97105C9.09865 9.10202 9.92598 8.42097 10.8922 8.00913C11.2193 7.87046 11.5606 7.7643 11.9083 7.69388C12.6297 7.54762 13.3731 7.54762 14.0945 7.69388C15.1312 7.90631 16.0825 8.41908 16.8299 9.1683L18.3639 7.63863C17.6725 6.94707 16.8546 6.39501 15.9546 6.01255C15.4956 5.81823 15.0184 5.67016 14.53 5.57055C13.5223 5.36581 12.4838 5.36581 11.4761 5.57055C10.9873 5.67057 10.5098 5.819 10.0504 6.01363C8.69682 6.58791 7.53808 7.54123 6.71374 8.7588C6.15895 9.5798 5.77099 10.5019 5.57191 11.4725C5.54158 11.6188 5.52533 11.7683 5.50366 11.9167H2.16699L6.50033 16.25L10.8337 11.9167ZM15.167 14.0834H18.3076L18.3065 14.092C18.0234 15.4806 17.205 16.7019 16.0282 17.4915C15.443 17.8882 14.7851 18.1651 14.0923 18.3062C13.3713 18.4525 12.6283 18.4525 11.9072 18.3062C11.2146 18.1648 10.5567 17.8879 9.97133 17.4915C9.68383 17.2971 9.41541 17.0758 9.16966 16.8307L7.63783 18.3625C8.32954 19.0539 9.14791 19.6056 10.0482 19.9875C10.5076 20.1825 10.9875 20.331 11.4728 20.4295C12.4801 20.6344 13.5184 20.6344 14.5257 20.4295C16.4676 20.0265 18.1757 18.8819 19.2869 17.2391C19.8412 16.4187 20.2288 15.4974 20.4277 14.5275C20.4569 14.3813 20.4742 14.2318 20.4959 14.0834H23.8337L19.5003 9.75005L15.167 14.0834Z" fill="#205CF5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -2,10 +2,10 @@ const IP_ADDRESS_REGEX = /([0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3})(:([0-9]{
|
|||||||
|
|
||||||
|
|
||||||
export class Utils {
|
export class Utils {
|
||||||
static isIpAddress = (address: string): boolean => IP_ADDRESS_REGEX.test(address)
|
static isIpAddress = (address: string): boolean => IP_ADDRESS_REGEX.test(address)
|
||||||
static lineNumbersInString = (code:string): number => code.split("\n").length;
|
static lineNumbersInString = (code: string): number => code.split("\n").length;
|
||||||
|
|
||||||
static humanFileSize(bytes, si=false, dp=1) {
|
static humanFileSize(bytes, si = false, dp = 1) {
|
||||||
const thresh = si ? 1000 : 1024;
|
const thresh = si ? 1000 : 1024;
|
||||||
|
|
||||||
if (Math.abs(bytes) < thresh) {
|
if (Math.abs(bytes) < thresh) {
|
||||||
@@ -16,7 +16,7 @@ export class Utils {
|
|||||||
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||||
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
|
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
|
||||||
let u = -1;
|
let u = -1;
|
||||||
const r = 10**dp;
|
const r = 10 ** dp;
|
||||||
|
|
||||||
do {
|
do {
|
||||||
bytes /= thresh;
|
bytes /= thresh;
|
||||||
@@ -26,4 +26,42 @@ export class Utils {
|
|||||||
|
|
||||||
return bytes.toFixed(dp) + ' ' + units[u];
|
return bytes.toFixed(dp) + ' ' + units[u];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static padTo2Digits = (num) => {
|
||||||
|
return String(num).padStart(2, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
static getHoursAndMinutes = (protocolTimeKey) => {
|
||||||
|
const time = new Date(protocolTimeKey)
|
||||||
|
const hoursAndMinutes = Utils.padTo2Digits(time.getHours()) + ':' + Utils.padTo2Digits(time.getMinutes());
|
||||||
|
return hoursAndMinutes;
|
||||||
|
}
|
||||||
|
|
||||||
|
static creatUniqueObjArrayByProp = (objArray, prop) => {
|
||||||
|
const map = new Map(objArray.map((item) => [item[prop], item])).values()
|
||||||
|
return Array.from(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
static isJson = (str) => {
|
||||||
|
try {
|
||||||
|
JSON.parse(str);
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static stringToColor = (str) => {
|
||||||
|
let colors = ["#e51c23", "#e91e63", "#9c27b0", "#673ab7", "#3f51b5", "#5677fc", "#03a9f4", "#00bcd4", "#009688", "#259b24", "#8bc34a", "#afb42b", "#ff9800", "#ff5722", "#795548", "#607d8b"]
|
||||||
|
|
||||||
|
let hash = 0;
|
||||||
|
if (str.length === 0) return hash;
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||||
|
hash = hash & hash;
|
||||||
|
}
|
||||||
|
hash = ((hash % colors.length) + colors.length) % colors.length;
|
||||||
|
return colors[hash];
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
24
ui-common/src/hooks/useDebounce.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
const useDebounce = (value, delay) => {
|
||||||
|
// State and setters for debounced value
|
||||||
|
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||||
|
useEffect(
|
||||||
|
() => {
|
||||||
|
// Update debounced value after delay
|
||||||
|
const handler = setTimeout(() => {
|
||||||
|
setDebouncedValue(value);
|
||||||
|
}, delay);
|
||||||
|
// Cancel the timeout if value changes (also on delay change or unmount)
|
||||||
|
// This is how we prevent debounced value from updating if value is changed ...
|
||||||
|
// .. within the delay period. Timeout gets cleared and restarted.
|
||||||
|
return () => {
|
||||||
|
clearTimeout(handler);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[value, delay] // Only re-call effect if value or delay changes
|
||||||
|
);
|
||||||
|
return debouncedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useDebounce
|
||||||