mirror of
https://github.com/buger/goreplay.git
synced 2024-04-21 12:32:02 +00:00
187 lines
4.8 KiB
Go
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|