Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ require (
github.com/google/uuid v1.3.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hexops/gotextdiff v1.0.3 // indirect
github.com/icholy/replace v0.6.0 // indirect
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
)
16 changes: 16 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,22 @@ github.com/elazarl/goproxy v0.0.0-20230731152917-f99041a5c027 h1:1L0aalTpPz7YlMx
github.com/elazarl/goproxy v0.0.0-20230731152917-f99041a5c027/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM=
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/icholy/replace v0.6.0 h1:EBiD2pGqZIOJAbEaf/5GVRaD/Pmbb4n+K3LrBdXd4dw=
github.com/icholy/replace v0.6.0/go.mod h1:zzi8pxElj2t/5wHHHYmH45D+KxytX/t4w3ClY5nlK+g=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
Expand All @@ -37,13 +42,24 @@ github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw=
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
165 changes: 165 additions & 0 deletions processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (

"github.com/aws/aws-sdk-go-v2/aws"
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
"github.com/icholy/replace"
"golang.org/x/exp/slices"
)

Expand All @@ -37,6 +38,16 @@ const (
// ParamSubtoken optionally specifies which subtoken should be used.
// Default is SubtokenAccessToken.
ParamSubtoken = "st"

// ParamPlaceholder specifies the placeholder pattern to replace in the
// request body with the token value. Used by InjectBodyProcessor.
ParamPlaceholder = "placeholder"
)

const (
// MaxBodySizeForInjection limits body size for placeholder replacement
// to prevent memory exhaustion (default: 10MB)
MaxBodySizeForInjection = 10 * 1024 * 1024
)

const (
Expand All @@ -54,7 +65,9 @@ type ProcessorConfig interface {
type wireProcessor struct {
InjectProcessorConfig *InjectProcessorConfig `json:"inject_processor,omitempty"`
InjectHMACProcessorConfig *InjectHMACProcessorConfig `json:"inject_hmac_processor,omitempty"`
InjectBodyProcessorConfig *InjectBodyProcessorConfig `json:"inject_body_processor,omitempty"`
OAuthProcessorConfig *OAuthProcessorConfig `json:"oauth2_processor,omitempty"`
OAuthBodyProcessorConfig *OAuthBodyProcessorConfig `json:"oauth2_body_processor,omitempty"`
Sigv4ProcessorConfig *Sigv4ProcessorConfig `json:"sigv4_processor,omitempty"`
MultiProcessorConfig *MultiProcessorConfig `json:"multi_processor,omitempty"`
}
Expand All @@ -65,8 +78,12 @@ func newWireProcessor(p ProcessorConfig) (wireProcessor, error) {
return wireProcessor{InjectProcessorConfig: p}, nil
case *InjectHMACProcessorConfig:
return wireProcessor{InjectHMACProcessorConfig: p}, nil
case *InjectBodyProcessorConfig:
return wireProcessor{InjectBodyProcessorConfig: p}, nil
case *OAuthProcessorConfig:
return wireProcessor{OAuthProcessorConfig: p}, nil
case *OAuthBodyProcessorConfig:
return wireProcessor{OAuthBodyProcessorConfig: p}, nil
case *Sigv4ProcessorConfig:
return wireProcessor{Sigv4ProcessorConfig: p}, nil
case *MultiProcessorConfig:
Expand All @@ -88,10 +105,18 @@ func (wp *wireProcessor) getProcessorConfig() (ProcessorConfig, error) {
np += 1
p = wp.InjectHMACProcessorConfig
}
if wp.InjectBodyProcessorConfig != nil {
np += 1
p = wp.InjectBodyProcessorConfig
}
if wp.OAuthProcessorConfig != nil {
np += 1
p = wp.OAuthProcessorConfig
}
if wp.OAuthBodyProcessorConfig != nil {
np += 1
p = wp.OAuthBodyProcessorConfig
}
if wp.Sigv4ProcessorConfig != nil {
np += 1
p = wp.Sigv4ProcessorConfig
Expand Down Expand Up @@ -198,6 +223,58 @@ func (c *InjectHMACProcessorConfig) StripHazmat() ProcessorConfig {
}
}

type InjectBodyProcessorConfig struct {
Token string `json:"token"`
Placeholder string `json:"placeholder,omitempty"`
}

var _ ProcessorConfig = new(InjectBodyProcessorConfig)

func (c *InjectBodyProcessorConfig) Processor(params map[string]string) (RequestProcessor, error) {
if c.Token == "" {
return nil, errors.New("missing token")
}

// Get placeholder from params or use config default or fallback
placeholder := c.Placeholder
if paramPlaceholder, ok := params[ParamPlaceholder]; ok {
placeholder = paramPlaceholder
}
if placeholder == "" {
placeholder = "{{ACCESS_TOKEN}}"
}

return func(r *http.Request) error {
if r.Body == nil {
return nil
}

// Use streaming replacement to avoid loading entire body into memory
chain := replace.Chain(r.Body, replace.String(placeholder, c.Token))

// Buffer the replaced content to calculate Content-Length
var buf bytes.Buffer
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hrm.. we still are reading the whole thing into memory (into buf) to get the content length. I think you can set the r.ContentLength to zero and r.Body to the replacement chain, and it should be able to stream the body using chunked encoding without knowing the content length ahead of time.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to use the 0 contentlength and r.Body = io.NopCloser(chain)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should do it I think, though I would try it out just to make sure. since this shouldnt break any existing use cases you can test it after deploying.

n, err := io.Copy(&buf, chain)
if err != nil {
return fmt.Errorf("failed to replace placeholder in request body: %w", err)
}
r.Body.Close()

// Set the new body with updated Content-Length
r.Body = io.NopCloser(&buf)
r.ContentLength = n

return nil
}, nil
}

func (c *InjectBodyProcessorConfig) StripHazmat() ProcessorConfig {
return &InjectBodyProcessorConfig{
Token: redactedStr,
Placeholder: c.Placeholder,
}
}

type OAuthProcessorConfig struct {
Token *OAuthToken `json:"token"`
}
Expand All @@ -219,6 +296,34 @@ func (c *OAuthProcessorConfig) Processor(params map[string]string) (RequestProce
return nil, errors.New("missing token")
}

// Check if placeholder parameter is present for body injection
if placeholder, ok := params[ParamPlaceholder]; ok && placeholder != "" {
// Inject token into request body by replacing placeholder
return func(r *http.Request) error {
if r.Body == nil {
return nil
}

// Use streaming replacement to avoid loading entire body into memory
chain := replace.Chain(r.Body, replace.String(placeholder, token))

// Buffer the replaced content to calculate Content-Length
var buf bytes.Buffer
n, err := io.Copy(&buf, chain)
if err != nil {
return fmt.Errorf("failed to replace placeholder in request body: %w", err)
}
r.Body.Close()

// Set the new body with updated Content-Length
r.Body = io.NopCloser(&buf)
r.ContentLength = n

return nil
}, nil
}

// Default behavior: inject into Authorization header
return func(r *http.Request) error {
r.Header.Set("Authorization", "Bearer "+token)
return nil
Expand All @@ -234,6 +339,66 @@ func (c *OAuthProcessorConfig) StripHazmat() ProcessorConfig {
}
}

type OAuthBodyProcessorConfig struct {
Token *OAuthToken `json:"token"`
Placeholder string `json:"placeholder,omitempty"`
}

var _ ProcessorConfig = (*OAuthBodyProcessorConfig)(nil)

func (c *OAuthBodyProcessorConfig) Processor(params map[string]string) (RequestProcessor, error) {
token := c.Token.AccessToken
if params[ParamSubtoken] == SubtokenRefresh {
token = c.Token.RefreshToken
}

if token == "" {
return nil, errors.New("missing token")
}

// Get placeholder from params or use config default or fallback
placeholder := c.Placeholder
if paramPlaceholder, ok := params[ParamPlaceholder]; ok {
placeholder = paramPlaceholder
}
if placeholder == "" {
placeholder = "{{ACCESS_TOKEN}}"
}

return func(r *http.Request) error {
if r.Body == nil {
return nil
}

// Use streaming replacement to avoid loading entire body into memory
chain := replace.Chain(r.Body, replace.String(placeholder, token))

// Buffer the replaced content to calculate Content-Length
var buf bytes.Buffer
n, err := io.Copy(&buf, chain)
if err != nil {
return fmt.Errorf("failed to replace placeholder in request body: %w", err)
}
r.Body.Close()

// Set the new body with updated Content-Length
r.Body = io.NopCloser(&buf)
r.ContentLength = n

return nil
}, nil
}

func (c *OAuthBodyProcessorConfig) StripHazmat() ProcessorConfig {
return &OAuthBodyProcessorConfig{
Token: &OAuthToken{
AccessToken: redactedStr,
RefreshToken: redactedStr,
},
Placeholder: c.Placeholder,
}
}

type Sigv4ProcessorConfig struct {
AccessKey string `json:"access_key"`
SecretKey string `json:"secret_key"`
Expand Down
Loading