Skip to content

Commit

Permalink
Added a --strict-redirect-location option; if enabled wiretap will re…
Browse files Browse the repository at this point in the history
…write all header hosts to wiretap's api gateway host
  • Loading branch information
jacobm-splunk authored and daveshanley committed Apr 28, 2024
1 parent bd63eae commit b7afdfb
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 60 deletions.
9 changes: 2 additions & 7 deletions cmd/booted_message.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
package cmd

import (
"fmt"
"github.com/pb33f/ranch/bus"
"github.com/pb33f/ranch/model"
"github.com/pb33f/ranch/plank/pkg/server"
Expand All @@ -21,13 +20,9 @@ func bootedMessage(wiretapConfig *shared.WiretapConfiguration) {
if !seen {
seen = true
pterm.Println()
protocol := "http"
if wiretapConfig.CertificateKey != "" && wiretapConfig.Certificate != "" {
protocol = "https"
}

b1 := pterm.DefaultBox.WithTitle(pterm.LightMagenta("API Gateway")).Sprint(fmt.Sprintf("%s://localhost:%s", protocol, wiretapConfig.Port))
b2 := pterm.DefaultBox.WithTitle(pterm.LightMagenta("Monitor UI")).Sprint(fmt.Sprintf("%s://localhost:%s", protocol, wiretapConfig.MonitorPort))
b1 := pterm.DefaultBox.WithTitle(pterm.LightMagenta("API Gateway")).Sprint(wiretapConfig.GetApiGateway())
b2 := pterm.DefaultBox.WithTitle(pterm.LightMagenta("Monitor UI")).Sprint(wiretapConfig.GetMonitorUI())
b3 := pterm.DefaultBox.WithTitle(pterm.LightMagenta("Static files served from")).Sprint(wiretapConfig.StaticDir)

var pp *pterm.PanelPrinter
Expand Down
15 changes: 10 additions & 5 deletions cmd/root_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ var (
hardErrorCode, _ = cmd.Flags().GetInt("hard-validation-code")
hardErrorReturnCode, _ = cmd.Flags().GetInt("hard-validation-return-code")
streamReport, _ := cmd.Flags().GetBool("stream-report")
strictRedirectLocation, _ := cmd.Flags().GetBool("strict-redirect-location")

portFlag, _ := cmd.Flags().GetString("port")
if portFlag != "" {
Expand Down Expand Up @@ -186,6 +187,11 @@ var (
config.StreamReport = true
}
}
if strictRedirectLocation {
if !config.StrictRedirectLocation {
config.StrictRedirectLocation = true
}
}

if reportFilename != "" {
config.ReportFile = reportFilename
Expand Down Expand Up @@ -214,6 +220,9 @@ var (
if streamReport {
config.StreamReport = true
}
if strictRedirectLocation {
config.StrictRedirectLocation = true
}
if base != "" {
config.Base = base
}
Expand Down Expand Up @@ -359,11 +368,6 @@ var (
printLoadedWebsockets(config.WebsocketConfigs)
}

if len(config.ValidationAllowList) > 0 {
config.CompileIgnoreValidations()
// TODO: add print command
}

if len(config.IgnoreValidation) > 0 {
config.CompileIgnoreValidations()
printLoadedIgnoreValidationPaths(config.IgnoreValidation)
Expand Down Expand Up @@ -658,6 +662,7 @@ func Execute(version, commit, date string, fs embed.FS) {
rootCmd.Flags().StringArrayP("har-allow", "j", nil, "Add a path to the HAR allow list, can use arg multiple times")
rootCmd.Flags().StringP("report-filename", "f", "wiretap-report.json", "Filename for any headless report generation output")
rootCmd.Flags().BoolP("stream-report", "a", false, "Stream violations to report JSON file as they occur (headless mode)")
rootCmd.Flags().BoolP("strict-redirect-location", "r", false, "Rewrite the redirect `Location` header on redirect responses to wiretap's API Gateway Host")

if err := rootCmd.Execute(); err != nil {
os.Exit(1)
Expand Down
44 changes: 44 additions & 0 deletions daemon/handle_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/gorilla/websocket"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"text/template"
Expand Down Expand Up @@ -187,6 +188,10 @@ func (ws *WiretapService) handleHttpRequest(request *model.Request) {
// wiretap needs to work from anywhere, so allow everything.
setCORSHeaders(headers)

if config.StrictRedirectLocation && is3xxStatusCode(returnedResponse.StatusCode) {
setStrictLocationHeader(config, headers)
}

// write headers
for k, v := range headers {
for _, j := range v {
Expand Down Expand Up @@ -367,6 +372,41 @@ func setCORSHeaders(headers map[string][]string) {
headers["Access-Control-Allow-Methods"] = []string{"OPTIONS,POST,GET,DELETE,PATCH,PUT"}
}

// setStrictLocationHeader rewrites any `Location` headers to wiretap's ApiGatewayHost. Some web servers specify
// the full URL when redirecting the browser, so we need to ensure that the browser isn't redirected away from the
// wiretap Host. We achieve this by rewriting the `Location` header host and port to wiretap's host and port on all
// redirect responses.
func setStrictLocationHeader(config *shared.WiretapConfiguration, headers map[string][]string) {
if locations, ok := headers["Location"]; ok {
newLocations := make([]string, 0)

apiGatewayHost := config.GetApiGatewayHost()

for _, location := range locations {
parsedLocation, parseErr := url.Parse(location)

// Unable to parse the location url, let's just re-add the location to ensure that there is at least one
// redirect target
if parseErr != nil {
config.Logger.Warn(fmt.Sprintf("Unable to parse `Location` header URL: %s", location))
newLocations = append(newLocations, location)

// Check if the target location's host differs from wiretap's host
} else if parsedLocation.Host != apiGatewayHost {
parsedLocation.Host = apiGatewayHost

newLocation := parsedLocation.String()
config.Logger.Info(fmt.Sprintf("Rewrote `Location` header from %s to %s", location, newLocation))

newLocations = append(newLocations, newLocation)
}

}
headers["Location"] = newLocations
}

}

func getCloseCode(err error) (int, bool) {
unexpectedClose := websocket.IsUnexpectedCloseError(err,
websocket.CloseNormalClosure,
Expand All @@ -381,6 +421,10 @@ func getCloseCode(err error) (int, bool) {
return -1, unexpectedClose
}

func is3xxStatusCode(statusCode int) bool {
return 300 <= statusCode && statusCode < 400
}

func logWebsocketClose(config *shared.WiretapConfiguration, closeCode int, isUnexpected bool) {
if isUnexpected {
config.Logger.Warn(fmt.Sprintf("Websocket closed unexepectedly with code: %d", closeCode))
Expand Down
119 changes: 71 additions & 48 deletions shared/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,54 +13,55 @@ 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"`
IgnoreRedirects []string `json:"ignoreRedirects,omitempty" yaml:"ignoreRedirects,omitempty"`
RedirectAllowList []string `json:"redirectAllowList,omitempty" yaml:"redirectAllowList,omitempty"`
WebsocketConfigs map[string]*WiretapWebsocketConfig `json:"websockets" yaml:"websockets"`
IgnoreValidation []string `json:"ignoreValidation,omitempty" yaml:"ignoreValidation,omitempty"`
ValidationAllowList []string `json:"validationAllowList,omitempty" yaml:"validationAllowList,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:"-"`
CompiledIgnoreValidations []*CompiledRedirect `json:"-" yaml:"-"`
CompiledValidationAllowList []*CompiledRedirect `json:"-" yaml:"-"`
FS embed.FS `json:"-"`
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"`
WebsocketConfigs map[string]*WiretapWebsocketConfig `json:"websockets" yaml:"websockets"`
IgnoreValidation []string `json:"ignoreValidation,omitempty" yaml:"ignoreValidation,omitempty"`
ValidationAllowList []string `json:"validationAllowList,omitempty" yaml:"validationAllowList,omitempty"`
StrictRedirectLocation bool `json:"strictRedirectLocation,omitempty" yaml:"strictRedirectLocation,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:"-"`
CompiledIgnoreValidations []*CompiledRedirect `json:"-" yaml:"-"`
CompiledValidationAllowList []*CompiledRedirect `json:"-" yaml:"-"`
FS embed.FS `json:"-"`
Logger *slog.Logger
}

Expand Down Expand Up @@ -150,6 +151,28 @@ func (wtc *WiretapConfiguration) ReplaceWithVariables(input string) string {
return input
}

func (wtc *WiretapConfiguration) GetHttpProtocol() string {
protocol := "http"

if wtc.CertificateKey != "" && wtc.Certificate != "" {
protocol = "https"
}

return protocol
}

func (wtc *WiretapConfiguration) GetApiGateway() string {
return fmt.Sprintf("%s://%s", wtc.GetHttpProtocol(), wtc.GetApiGatewayHost())
}

func (wtc *WiretapConfiguration) GetApiGatewayHost() string {
return fmt.Sprintf("localhost:%s", wtc.Port)
}

func (wtc *WiretapConfiguration) GetMonitorUI() string {
return fmt.Sprintf("%s://localhost:%s", wtc.GetHttpProtocol(), wtc.MonitorPort)
}

type WiretapWebsocketConfig struct {
VerifyCert *bool `json:"verifyCert" yaml:"verifyCert"`
DropHeaders []string `json:"dropHeaders" yaml:"dropHeaders"`
Expand Down

0 comments on commit b7afdfb

Please sign in to comment.