diff --git a/cmd/root_command.go b/cmd/root_command.go index 6c0eedd..76a57fe 100644 --- a/cmd/root_command.go +++ b/cmd/root_command.go @@ -339,6 +339,16 @@ var ( printLoadedPathDelayConfigurations(config.PathDelays) } + if len(config.IgnoreRedirects) > 0 { + config.CompileIgnoreRedirects() + printLoadedIgnoreRedirectPaths(config.IgnoreRedirects) + } + + if len(config.RedirectAllowList) > 0 { + config.CompileRedirectAllowList() + printLoadedRedirectAllowList(config.RedirectAllowList) + } + // static headers if config.Headers != nil && len(config.Headers.DropHeaders) > 0 { pterm.Info.Printf("Dropping the following %d %s globally:\n", len(config.Headers.DropHeaders), @@ -684,3 +694,23 @@ func printLoadedHarWhitelist(variables []string) { } pterm.Println() } + +func printLoadedIgnoreRedirectPaths(ignoreRedirects []string) { + pterm.Info.Printf("Loaded %d %s to ignore for redirects:\n", len(ignoreRedirects), + shared.Pluralize(len(ignoreRedirects), "path", "paths")) + + for _, x := range ignoreRedirects { + pterm.Printf("🙈 Paths matching '%s' will be ignored for resolving redirects\n", pterm.LightCyan(x)) + } + pterm.Println() +} + +func printLoadedRedirectAllowList(allowRedirects []string) { + pterm.Info.Printf("Loaded %d allows listed redirect %s :\n", len(allowRedirects), + shared.Pluralize(len(allowRedirects), "path", "paths")) + + for _, x := range allowRedirects { + pterm.Printf("🐵 Paths matching '%s' will always follow redirects, regardless of ignoreRedirect settings\n", pterm.LightCyan(x)) + } + pterm.Println() +} diff --git a/config/paths.go b/config/paths.go index 30572c4..2def94b 100644 --- a/config/paths.go +++ b/config/paths.go @@ -29,6 +29,24 @@ func FindPathDelay(path string, configuration *shared.WiretapConfiguration) int return foundMatch } +func IgnoreRedirectOnPath(path string, configuration *shared.WiretapConfiguration) bool { + for _, redirectPath := range configuration.CompiledIgnoreRedirects { + if redirectPath.CompiledPath.Match(path) { + return true + } + } + return false +} + +func PathRedirectAllowListed(path string, configuration *shared.WiretapConfiguration) bool { + for _, redirectPath := range configuration.CompiledRedirectAllowList { + if redirectPath.CompiledPath.Match(path) { + return true + } + } + return false +} + func RewritePath(path string, configuration *shared.WiretapConfiguration) string { paths := FindPaths(path, configuration) var replaced string = path diff --git a/config/paths_test.go b/config/paths_test.go index 1504660..96be2ea 100644 --- a/config/paths_test.go +++ b/config/paths_test.go @@ -214,3 +214,97 @@ func TestLocatePathDelay(t *testing.T) { assert.Equal(t, 0, delay) } + +func TestIgnoreRedirect(t *testing.T) { + + config := `ignoreRedirects: + - /pb33f/test/** + - /pb33f/cakes/123 + - /*/test/123` + + var c shared.WiretapConfiguration + _ = yaml.Unmarshal([]byte(config), &c) + + c.CompileIgnoreRedirects() + + ignore := IgnoreRedirectOnPath("/pb33f/test/burgers/fries?1234=no", &c) + assert.True(t, ignore) + + ignore = IgnoreRedirectOnPath("/pb33f/cakes/123", &c) + assert.True(t, ignore) + + ignore = IgnoreRedirectOnPath("/roastbeef/test/123", &c) + assert.True(t, ignore) + + ignore = IgnoreRedirectOnPath("/not-registered", &c) + assert.False(t, ignore) + +} + +func TestIgnoreRedirect_NoPathsRegistered(t *testing.T) { + + var c shared.WiretapConfiguration + _ = yaml.Unmarshal([]byte(""), &c) + + c.CompileIgnoreRedirects() + + ignore := IgnoreRedirectOnPath("/pb33f/test/burgers/fries?1234=no", &c) + assert.False(t, ignore) + + ignore = IgnoreRedirectOnPath("/pb33f/cakes/123", &c) + assert.False(t, ignore) + + ignore = IgnoreRedirectOnPath("/roastbeef/test/123", &c) + assert.False(t, ignore) + + ignore = IgnoreRedirectOnPath("/not-registered", &c) + assert.False(t, ignore) + +} + +func TestRedirectAllowList(t *testing.T) { + + config := `ignoreRedirects: + - /pb33f/test/** + - /pb33f/cakes/123 + - /*/test/123` + + var c shared.WiretapConfiguration + _ = yaml.Unmarshal([]byte(config), &c) + + c.CompileIgnoreRedirects() + + ignore := IgnoreRedirectOnPath("/pb33f/test/burgers/fries?1234=no", &c) + assert.True(t, ignore) + + ignore = IgnoreRedirectOnPath("/pb33f/cakes/123", &c) + assert.True(t, ignore) + + ignore = IgnoreRedirectOnPath("/roastbeef/test/123", &c) + assert.True(t, ignore) + + ignore = IgnoreRedirectOnPath("/not-registered", &c) + assert.False(t, ignore) + +} + +func TestRedirectAllowList_NoPathsRegistered(t *testing.T) { + + var c shared.WiretapConfiguration + _ = yaml.Unmarshal([]byte(""), &c) + + c.CompileIgnoreRedirects() + + ignore := IgnoreRedirectOnPath("/pb33f/test/burgers/fries?1234=no", &c) + assert.False(t, ignore) + + ignore = IgnoreRedirectOnPath("/pb33f/cakes/123", &c) + assert.False(t, ignore) + + ignore = IgnoreRedirectOnPath("/roastbeef/test/123", &c) + assert.False(t, ignore) + + ignore = IgnoreRedirectOnPath("/not-registered", &c) + assert.False(t, ignore) + +} diff --git a/daemon/api.go b/daemon/api.go index 8baa00e..3dfcf6a 100644 --- a/daemon/api.go +++ b/daemon/api.go @@ -5,11 +5,10 @@ package daemon import ( "crypto/tls" - "net/http" - "net/url" - "github.com/pb33f/wiretap/config" "github.com/pterm/pterm" + "net/http" + "net/url" "github.com/pb33f/wiretap/shared" ) @@ -40,9 +39,6 @@ func (c *wiretapTransport) RoundTrip(r *http.Request) (*http.Response, error) { func (ws *WiretapService) callAPI(req *http.Request) (*http.Response, error) { - tr := newWiretapTransport() - client := &http.Client{Transport: tr} - configStore, _ := ws.controlsStore.Get(shared.ConfigKey) // create a new request from the original request, but replace the path @@ -59,6 +55,21 @@ func (ws *WiretapService) callAPI(req *http.Request) (*http.Response, error) { req.URL = newUrl } + tr := newWiretapTransport() + var client *http.Client + + // create a client based on if wiretap should redirect on the path or not + if config.IgnoreRedirectOnPath(req.URL.Path, wiretapConfig) && !config.PathRedirectAllowListed(req.URL.Path, wiretapConfig) { + client = &http.Client{ + Transport: tr, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + } else { + client = &http.Client{Transport: tr} + } + // re-write referer if req.Header.Get("Referer") != "" { // retain original referer for logging diff --git a/shared/config.go b/shared/config.go index 5ab3d12..05a9a47 100644 --- a/shared/config.go +++ b/shared/config.go @@ -13,46 +13,50 @@ import ( ) type WiretapConfiguration struct { - Contract string `json:"-" yaml:"-"` - RedirectHost string `json:"redirectHost,omitempty" yaml:"redirectHost,omitempty"` - RedirectPort string `json:"redirectPort,omitempty" yaml:"redirectPort,omitempty"` - RedirectBasePath string `json:"redirectBasePath,omitempty" yaml:"redirectBasePath,omitempty"` - RedirectProtocol string `json:"redirectProtocol,omitempty" yaml:"redirectProtocol,omitempty"` - RedirectURL string `json:"redirectURL,omitempty" yaml:"redirectURL,omitempty"` - Port string `json:"port,omitempty" yaml:"port,omitempty"` - MonitorPort string `json:"monitorPort,omitempty" yaml:"monitorPort,omitempty"` - WebSocketHost string `json:"webSocketHost,omitempty" yaml:"webSocketHost,omitempty"` - WebSocketPort string `json:"webSocketPort,omitempty" yaml:"webSocketPort,omitempty"` - GlobalAPIDelay int `json:"globalAPIDelay,omitempty" yaml:"globalAPIDelay,omitempty"` - StaticDir string `json:"staticDir,omitempty" yaml:"staticDir,omitempty"` - StaticIndex string `json:"staticIndex,omitempty" yaml:"staticIndex,omitempty"` - PathConfigurations map[string]*WiretapPathConfig `json:"paths,omitempty" yaml:"paths,omitempty"` - Headers *WiretapHeaderConfig `json:"headers,omitempty" yaml:"headers,omitempty"` - StaticPaths []string `json:"staticPaths,omitempty" yaml:"staticPaths,omitempty"` - Variables map[string]string `json:"variables,omitempty" yaml:"variables,omitempty"` - Spec string `json:"contract,omitempty" yaml:"contract,omitempty"` - Certificate string `json:"certificate,omitempty" yaml:"certificate,omitempty"` - CertificateKey string `json:"certificateKey,omitempty" yaml:"certificateKey,omitempty"` - HardErrors bool `json:"hardValidation,omitempty" yaml:"hardValidation,omitempty"` - HardErrorCode int `json:"hardValidationCode,omitempty" yaml:"hardValidationCode,omitempty"` - HardErrorReturnCode int `json:"hardValidationReturnCode,omitempty" yaml:"hardValidationReturnCode,omitempty"` - PathDelays map[string]int `json:"pathDelays,omitempty" yaml:"pathDelays,omitempty"` - MockMode bool `json:"mockMode,omitempty" yaml:"mockMode,omitempty"` - MockModePretty bool `json:"mockModePretty,omitempty" yaml:"mockModePretty,omitempty"` - Base string `json:"base,omitempty" yaml:"base,omitempty"` - HAR string `json:"har,omitempty" yaml:"har,omitempty"` - HARValidate bool `json:"harValidate,omitempty" yaml:"harValidate,omitempty"` - HARPathAllowList []string `json:"harPathAllowList,omitempty" yaml:"harPathAllowList,omitempty"` - StreamReport bool `json:"streamReport,omitempty" yaml:"streamReport,omitempty"` - ReportFile string `json:"reportFilename,omitempty" yaml:"reportFilename,omitempty"` - HARFile *harhar.HAR `json:"-" yaml:"-"` - CompiledPathDelays map[string]*CompiledPathDelay `json:"-" yaml:"-"` - CompiledVariables map[string]*CompiledVariable `json:"-" yaml:"-"` - Version string `json:"-" yaml:"-"` - StaticPathsCompiled []glob.Glob `json:"-" yaml:"-"` - CompiledPaths map[string]*CompiledPath `json:"-"` - FS embed.FS `json:"-"` - Logger *slog.Logger + Contract string `json:"-" yaml:"-"` + RedirectHost string `json:"redirectHost,omitempty" yaml:"redirectHost,omitempty"` + RedirectPort string `json:"redirectPort,omitempty" yaml:"redirectPort,omitempty"` + RedirectBasePath string `json:"redirectBasePath,omitempty" yaml:"redirectBasePath,omitempty"` + RedirectProtocol string `json:"redirectProtocol,omitempty" yaml:"redirectProtocol,omitempty"` + RedirectURL string `json:"redirectURL,omitempty" yaml:"redirectURL,omitempty"` + Port string `json:"port,omitempty" yaml:"port,omitempty"` + MonitorPort string `json:"monitorPort,omitempty" yaml:"monitorPort,omitempty"` + WebSocketHost string `json:"webSocketHost,omitempty" yaml:"webSocketHost,omitempty"` + WebSocketPort string `json:"webSocketPort,omitempty" yaml:"webSocketPort,omitempty"` + GlobalAPIDelay int `json:"globalAPIDelay,omitempty" yaml:"globalAPIDelay,omitempty"` + StaticDir string `json:"staticDir,omitempty" yaml:"staticDir,omitempty"` + StaticIndex string `json:"staticIndex,omitempty" yaml:"staticIndex,omitempty"` + PathConfigurations map[string]*WiretapPathConfig `json:"paths,omitempty" yaml:"paths,omitempty"` + Headers *WiretapHeaderConfig `json:"headers,omitempty" yaml:"headers,omitempty"` + StaticPaths []string `json:"staticPaths,omitempty" yaml:"staticPaths,omitempty"` + Variables map[string]string `json:"variables,omitempty" yaml:"variables,omitempty"` + Spec string `json:"contract,omitempty" yaml:"contract,omitempty"` + Certificate string `json:"certificate,omitempty" yaml:"certificate,omitempty"` + CertificateKey string `json:"certificateKey,omitempty" yaml:"certificateKey,omitempty"` + HardErrors bool `json:"hardValidation,omitempty" yaml:"hardValidation,omitempty"` + HardErrorCode int `json:"hardValidationCode,omitempty" yaml:"hardValidationCode,omitempty"` + HardErrorReturnCode int `json:"hardValidationReturnCode,omitempty" yaml:"hardValidationReturnCode,omitempty"` + PathDelays map[string]int `json:"pathDelays,omitempty" yaml:"pathDelays,omitempty"` + MockMode bool `json:"mockMode,omitempty" yaml:"mockMode,omitempty"` + MockModePretty bool `json:"mockModePretty,omitempty" yaml:"mockModePretty,omitempty"` + Base string `json:"base,omitempty" yaml:"base,omitempty"` + HAR string `json:"har,omitempty" yaml:"har,omitempty"` + HARValidate bool `json:"harValidate,omitempty" yaml:"harValidate,omitempty"` + HARPathAllowList []string `json:"harPathAllowList,omitempty" yaml:"harPathAllowList,omitempty"` + StreamReport bool `json:"streamReport,omitempty" yaml:"streamReport,omitempty"` + ReportFile string `json:"reportFilename,omitempty" yaml:"reportFilename,omitempty"` + IgnoreRedirects []string `json:"ignoreRedirects,omitempty" yaml:"ignoreRedirects,omitempty"` + RedirectAllowList []string `json:"redirectAllowList,omitempty" yaml:"redirectAllowList,omitempty"` + HARFile *harhar.HAR `json:"-" yaml:"-"` + CompiledPathDelays map[string]*CompiledPathDelay `json:"-" yaml:"-"` + CompiledVariables map[string]*CompiledVariable `json:"-" yaml:"-"` + Version string `json:"-" yaml:"-"` + StaticPathsCompiled []glob.Glob `json:"-" yaml:"-"` + CompiledPaths map[string]*CompiledPath `json:"-"` + CompiledIgnoreRedirects []*CompiledRedirect `json:"-" yaml:"-"` + CompiledRedirectAllowList []*CompiledRedirect `json:"-" yaml:"-"` + FS embed.FS `json:"-"` + Logger *slog.Logger } func (wtc *WiretapConfiguration) CompilePaths() { @@ -91,6 +95,26 @@ func (wtc *WiretapConfiguration) CompileVariables() { } } +func (wtc *WiretapConfiguration) CompileIgnoreRedirects() { + wtc.CompiledIgnoreRedirects = make([]*CompiledRedirect, 0) + for _, x := range wtc.IgnoreRedirects { + compiled := &CompiledRedirect{ + CompiledPath: glob.MustCompile(wtc.ReplaceWithVariables(x)), + } + wtc.CompiledIgnoreRedirects = append(wtc.CompiledIgnoreRedirects, compiled) + } +} + +func (wtc *WiretapConfiguration) CompileRedirectAllowList() { + wtc.CompiledRedirectAllowList = make([]*CompiledRedirect, 0) + for _, x := range wtc.RedirectAllowList { + compiled := &CompiledRedirect{ + CompiledPath: glob.MustCompile(wtc.ReplaceWithVariables(x)), + } + wtc.CompiledRedirectAllowList = append(wtc.CompiledRedirectAllowList, compiled) + } +} + func (wtc *WiretapConfiguration) ReplaceWithVariables(input string) string { for x := range wtc.Variables { if wtc.Variables[x] != "" && wtc.CompiledVariables[x] != nil { @@ -135,6 +159,10 @@ type CompiledPathRewrite struct { CompiledTarget glob.Glob } +type CompiledRedirect struct { + CompiledPath glob.Glob +} + type WiretapHeaderConfig struct { DropHeaders []string `json:"drop,omitempty" yaml:"drop,omitempty"` InjectHeaders map[string]string `json:"inject,omitempty" yaml:"inject,omitempty"`