Files
goreplay/pkg/emitter/emitter.go
T
2023-03-07 12:29:25 +03:00

187 lines
4.8 KiB
Go

package emitter
import (
"hash/fnv"
"io"
"sync"
"github.com/buger/goreplay/internal/byteutils"
"github.com/buger/goreplay/internal/size"
"github.com/buger/goreplay/pkg/http_modifier"
"github.com/buger/goreplay/pkg/middleware"
"github.com/buger/goreplay/pkg/plugin"
"github.com/buger/goreplay/pkg/prettify"
"github.com/buger/goreplay/pkg/pro"
"github.com/buger/goreplay/pkg/proto"
"github.com/coocood/freecache"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
var logger = log.With().Str("component", "emitter").Logger()
// Emitter represents an abject to manage plugins communication
type Emitter struct {
sync.WaitGroup
plugins *plugin.InOutPlugins
config *Config
}
// Config represents configuration for Emitter
type Config struct {
CopyBufferSize size.Size `json:"copy-buffer-size"`
Middleware string `json:"middleware"`
ModifierConfig http_modifier.HTTPModifierConfig
PrettifyHTTP bool `json:"prettify-http"`
SplitOutput bool `json:"split-output"`
RecognizeTCPSessions bool `json:"recognize-tcp-sessions"`
}
// New creates and initializes new Emitter object.
func New(configs ...*Config) *Emitter {
config := &Config{}
if len(configs) > 0 {
config = configs[0]
}
return &Emitter{
config: config,
}
}
// Start initialize loop for sending data from inputs to outputs
func (e *Emitter) Start(plugins *plugin.InOutPlugins) {
if e.config.CopyBufferSize < 1 {
e.config.CopyBufferSize = 5 << 20
}
e.plugins = plugins
if e.config.Middleware != "" {
middleware := middleware.NewMiddleware(e.config.Middleware, nil)
for _, in := range plugins.Inputs {
middleware.ReadFrom(in)
}
e.plugins.Inputs = append(e.plugins.Inputs, middleware)
e.plugins.All = append(e.plugins.All, middleware)
e.Add(1)
go func() {
defer e.Done()
if err := e.CopyMulty(middleware, plugins.Outputs...); err != nil {
logger.Error().Err(err).Msg("error during copy")
}
}()
} else {
for _, in := range plugins.Inputs {
e.Add(1)
go func(in plugin.Reader) {
defer e.Done()
if err := e.CopyMulty(in, plugins.Outputs...); err != nil {
logger.Error().Err(err).Msg("error during copy")
}
}(in)
}
}
}
// Close closes all the goroutine and waits for it to finish.
func (e *Emitter) Close() {
for _, p := range e.plugins.All {
if cp, ok := p.(io.Closer); ok {
cp.Close()
}
}
if len(e.plugins.All) > 0 {
// wait for everything to stop
e.Wait()
}
e.plugins.All = nil // avoid Close to make changes again
}
// CopyMulty copies from 1 reader to multiple writers
func (e *Emitter) CopyMulty(src plugin.Reader, writers ...plugin.Writer) error {
wIndex := 0
modifier := http_modifier.NewHTTPModifier(&e.config.ModifierConfig)
filteredRequests := freecache.NewCache(200 * 1024 * 1024) // 200M
for {
msg, err := src.PluginRead()
if err != nil {
if err == plugin.ErrorStopped || err == io.EOF {
return nil
}
return err
}
if msg != nil && len(msg.Data) > 0 {
if len(msg.Data) > int(e.config.CopyBufferSize) {
msg.Data = msg.Data[:e.config.CopyBufferSize]
}
meta := proto.PayloadMeta(msg.Meta)
if len(meta) < 3 {
logger.Warn().Msgf("Found malformed record %q from %q", msg.Meta, src)
continue
}
requestID := meta[1]
// start a subroutine only when necessary
if log.Logger.GetLevel() == zerolog.DebugLevel {
logger.Debug().Msgf("input: %s from: %s", byteutils.SliceToString(msg.Meta[:len(msg.Meta)-1]), src)
}
if modifier != nil {
logger.Debug().Msgf("modifier: %s from: %s", requestID, src)
if proto.IsRequestPayload(msg.Meta) {
msg.Data = modifier.Rewrite(msg.Data)
// If modifier tells to skip request
if len(msg.Data) == 0 {
filteredRequests.Set(requestID, []byte{}, 60) //
continue
}
logger.Debug().Msgf("Rewritten input: %s from: %s", requestID, src)
} else {
_, err := filteredRequests.Get(requestID)
if err == nil {
filteredRequests.Del(requestID)
continue
}
}
}
if e.config.PrettifyHTTP {
msg.Data = prettify.PrettifyHTTP(msg.Data)
if len(msg.Data) == 0 {
continue
}
}
if e.config.SplitOutput {
if e.config.RecognizeTCPSessions {
if !pro.PRO {
logger.Fatal().Msg("Detailed TCP sessions work only with PRO license")
}
hasher := fnv.New32a()
hasher.Write(meta[1])
wIndex = int(hasher.Sum32()) % len(writers)
if _, err := writers[wIndex].PluginWrite(msg); err != nil {
return err
}
} else {
// Simple round robin
if _, err := writers[wIndex].PluginWrite(msg); err != nil {
return err
}
wIndex = (wIndex + 1) % len(writers)
}
} else {
for _, dst := range writers {
if _, err := dst.PluginWrite(msg); err != nil && err != io.ErrClosedPipe {
return err
}
}
}
}
}
}