Skip to content

Commit

Permalink
Merge pull request #438 from miheer/headers
Browse files Browse the repository at this point in the history
NE-1141: Adds logic for setting and deleting headers via Ingress Operator CR and Route Object.
  • Loading branch information
openshift-merge-robot committed Aug 9, 2023
2 parents 8c552ab + e68f171 commit 44a5304
Show file tree
Hide file tree
Showing 27 changed files with 4,672 additions and 292 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ require (
github.com/gocarina/gocsv v0.0.0-20190927101021-3ecffd272576
github.com/google/go-cmp v0.5.9
github.com/haproxytech/config-parser/v4 v4.0.0-rc1
github.com/openshift/api v0.0.0-20230120195050-6ba31fa438f2
github.com/openshift/api v0.0.0-20230807132801-600991d550ac
github.com/openshift/client-go v0.0.0-20230120202327-72f107311084
github.com/openshift/library-go v0.0.0-20230120202744-256994f916c4
github.com/prometheus/client_golang v1.14.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -282,8 +282,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/onsi/ginkgo/v2 v2.9.1 h1:zie5Ly042PD3bsCvsSOPvRnFwyo3rKe64TJlD6nu0mk=
github.com/onsi/gomega v1.27.4 h1:Z2AnStgsdSayCMDiCU42qIz+HLqEPcgiOCXjAU/w+8E=
github.com/openshift/api v0.0.0-20230120195050-6ba31fa438f2 h1:+nw0/d4spq880W7S74Twi5YU2ulsl3/a9o4OEZptYp0=
github.com/openshift/api v0.0.0-20230120195050-6ba31fa438f2/go.mod h1:ctXNyWanKEjGj8sss1KjjHQ3ENKFm33FFnS5BKaIPh4=
github.com/openshift/api v0.0.0-20230807132801-600991d550ac h1:HqT8MmYGXiUGUW0BjygTGOOvqO2wIsTaG3q8nboJyPY=
github.com/openshift/api v0.0.0-20230807132801-600991d550ac/go.mod h1:yimSGmjsI+XF1mr+AKBs2//fSXIOhhetHGbMlBEfXbs=
github.com/openshift/client-go v0.0.0-20230120202327-72f107311084 h1:66uaqNwA+qYyQDwsMWUfjjau8ezmg1dzCqub13KZOcE=
github.com/openshift/client-go v0.0.0-20230120202327-72f107311084/go.mod h1:M3h9m001PWac3eAudGG3isUud6yBjr5XpzLYLLTlHKo=
github.com/openshift/library-go v0.0.0-20230120202744-256994f916c4 h1:cFYg18OROQMHlrGWL9HpV1elDKbnRFLz/ED5VvP3qvw=
Expand Down
63 changes: 63 additions & 0 deletions images/router/haproxy/conf/haproxy-config.template
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,22 @@ frontend public
acl secure_redirect base,map_reg_int(/var/lib/haproxy/conf/os_route_http_redirect.map) -m bool
redirect scheme https if secure_redirect

{{- range $idx, $http_request_header := .HTTPRequestHeaders }}
{{- if eq $http_request_header.Action "Set" }}
http-request set-header {{ $http_request_header.Name }} {{ $http_request_header.Value }}
{{- else if eq $http_request_header.Action "Delete" }}
http-request del-header {{ $http_request_header.Name }}
{{- end }}
{{- end }}

{{- range $idx, $http_response_header := .HTTPResponseHeaders }}
{{- if eq $http_response_header.Action "Set" }}
http-response set-header {{ $http_response_header.Name }} {{ $http_response_header.Value }}
{{- else if eq $http_response_header.Action "Delete" }}
http-response del-header {{ $http_response_header.Name }}
{{- end }}
{{- end }}

use_backend %[base,map_reg(/var/lib/haproxy/conf/os_http_be.map)]

default_backend openshift_default
Expand Down Expand Up @@ -363,6 +379,22 @@ frontend fe_sni
http-request set-header X-SSL-Client-DER %{+Q}[ssl_c_der,base64]
{{- end }}

{{- range $idx, $http_request_header := .HTTPRequestHeaders }}
{{- if eq $http_request_header.Action "Set" }}
http-request set-header {{ $http_request_header.Name }} {{ $http_request_header.Value }}
{{- else if eq $http_request_header.Action "Delete" }}
http-request del-header {{ $http_request_header.Name }}
{{- end }}
{{- end }}

{{- range $idx, $http_response_header := .HTTPResponseHeaders }}
{{- if eq $http_response_header.Action "Set" }}
http-response set-header {{ $http_response_header.Name }} {{ $http_response_header.Value }}
{{- else if eq $http_response_header.Action "Delete" }}
http-response del-header {{ $http_response_header.Name }}
{{- end }}
{{- end }}

# map to backend
# Search from most specific to general path (host case).
# Note: If no match, haproxy uses the default_backend, no other
Expand Down Expand Up @@ -447,6 +479,22 @@ frontend fe_no_sni
http-request set-header X-SSL-Client-DER %{+Q}[ssl_c_der,base64]
{{- end }}

{{- range $idx, $http_request_header := .HTTPRequestHeaders }}
{{- if eq $http_request_header.Action "Set" }}
http-request set-header {{ $http_request_header.Name }} {{ $http_request_header.Value }}
{{- else if eq $http_request_header.Action "Delete" }}
http-request del-header {{ $http_request_header.Name }}
{{- end }}
{{- end }}

{{- range $idx, $http_response_header := .HTTPResponseHeaders }}
{{- if eq $http_response_header.Action "Set" }}
http-response set-header {{ $http_response_header.Name }} {{ $http_response_header.Value }}
{{- else if eq $http_response_header.Action "Delete" }}
http-response del-header {{ $http_response_header.Name }}
{{- end }}
{{- end }}

# map to backend
# Search from most specific to general path (host case).
# Note: If no match, haproxy uses the default_backend, no other
Expand Down Expand Up @@ -632,6 +680,21 @@ backend {{ genBackendNamePrefix $cfg.TLSTermination }}:{{ $cfgIdx }}
{{- end }}{{/* hsts header */}}
{{- end }}{{/* is "edge" or "reencrypt" */}}

{{- range $idx, $http_request_header := $cfg.HTTPRequestHeaders }}
{{- if eq $http_request_header.Action "Set" }}
http-request set-header {{ $http_request_header.Name }} {{ $http_request_header.Value }}
{{- else if eq $http_request_header.Action "Delete" }}
http-request del-header {{ $http_request_header.Name }}
{{- end }}
{{- end }}
{{- range $idx, $http_response_header := $cfg.HTTPResponseHeaders }}
{{- if eq $http_response_header.Action "Set" }}
http-response set-header {{ $http_response_header.Name }} {{ $http_response_header.Value }}
{{- else if eq $http_response_header.Action "Delete" }}
http-response del-header {{ $http_response_header.Name }}
{{- end }}
{{- end }}

{{- range $serviceUnitName, $weight := $cfg.ServiceUnitNames }}
{{- if ge $weight 0 }}{{/* weight=0 is reasonable to keep existing connections to backends with cookies as we can see the HTTP headers */}}
{{- with $serviceUnit := index $.ServiceUnits $serviceUnitName }}
Expand Down
124 changes: 124 additions & 0 deletions pkg/cmd/infra/router/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ type TemplateRouter struct {
CaptureHTTPCookie *templateplugin.CaptureHTTPCookie
HTTPHeaderNameCaseAdjustmentsString string
HTTPHeaderNameCaseAdjustments []templateplugin.HTTPHeaderNameCaseAdjustment
HTTPResponseHeadersString string
HTTPResponseHeaders []templateplugin.HTTPHeader
HTTPRequestHeadersString string
HTTPRequestHeaders []templateplugin.HTTPHeader

TemplateRouterConfigManager
}
Expand Down Expand Up @@ -182,6 +186,8 @@ func (o *TemplateRouter) Bind(flag *pflag.FlagSet) {
flag.StringVar(&o.CaptureHTTPResponseHeadersString, "capture-http-response-headers", env("ROUTER_CAPTURE_HTTP_RESPONSE_HEADERS", ""), "A comma-delimited list of HTTP response header names and maximum header value lengths that should be captured for logging. Each item must have the following form: name:maxLength")
flag.StringVar(&o.CaptureHTTPCookieString, "capture-http-cookie", env("ROUTER_CAPTURE_HTTP_COOKIE", ""), "Name and maximum length of HTTP cookie that should be captured for logging. The argument must have the following form: name:maxLength. Append '=' to the name to indicate that an exact match should be performed; otherwise a prefix match will be performed. The value of first cookie that matches the name is captured.")
flag.StringVar(&o.HTTPHeaderNameCaseAdjustmentsString, "http-header-name-case-adjustments", env("ROUTER_H1_CASE_ADJUST", ""), "A comma-delimited list of HTTP header names that should have their case adjusted. Each item must be a valid HTTP header name and should have the desired capitalization.")
flag.StringVar(&o.HTTPResponseHeadersString, "set-delete-http-response-header", env("ROUTER_HTTP_RESPONSE_HEADERS", ""), "A comma-delimited list of HTTP response header names and values that should be set/deleted.")
flag.StringVar(&o.HTTPRequestHeadersString, "set-delete-http-request-header", env("ROUTER_HTTP_REQUEST_HEADERS", ""), "A comma-delimited list of HTTP request header names and values that should be set/deleted.")
}

type RouterStats struct {
Expand Down Expand Up @@ -287,6 +293,106 @@ func parseCaptureHeaders(in string) ([]templateplugin.CaptureHTTPHeader, error)
return captureHeaders, nil
}

func parseHeadersToBeSetOrDeleted(in string) ([]templateplugin.HTTPHeader, error) {
var captureHeaders []templateplugin.HTTPHeader
var capture templateplugin.HTTPHeader
var err error
if len(in) == 0 {
return captureHeaders, fmt.Errorf("encoded header string not present")
}
if len(in) > 0 {
for _, header := range strings.Split(in, ",") {
parts := strings.Split(header, ":")
num := len(parts)
switch num {
default:
return captureHeaders, fmt.Errorf("invalid HTTP header input specification: %v", header)
case 3:
{
headerName, err := url.QueryUnescape(parts[0])
if err != nil {
return captureHeaders, fmt.Errorf("failed to decode percent encoding: %v", parts[0])
}
err = checkValidHeaderName(headerName)
if err != nil {
return captureHeaders, err
}
sanitizedHeaderName := templateplugin.SanitizeHeaderValue(headerName)
headerValue, err := url.QueryUnescape(parts[1])
if err != nil {
return captureHeaders, fmt.Errorf("failed to decode percent encoding: %v", parts[1])
}
sanitizedHeaderValue := templateplugin.SanitizeHeaderValue(headerValue)
action, err := url.QueryUnescape(parts[2])
if err != nil {
return captureHeaders, fmt.Errorf("failed to decode percent encoding: %v", parts[2])
}
err = checkValidAction(action)
if err != nil {
return captureHeaders, err
}
capture = templateplugin.HTTPHeader{
Name: sanitizedHeaderName,
Value: sanitizedHeaderValue,
Action: routev1.RouteHTTPHeaderActionType(action),
}
captureHeaders = append(captureHeaders, capture)
}
case 2:
{
headerName, err := url.QueryUnescape(parts[0])
if err != nil {
return captureHeaders, fmt.Errorf("failed to decode percent encoding: %v", parts[0])
}
err = checkValidHeaderName(headerName)
if err != nil {
return captureHeaders, err
}
sanitizedHeaderName := templateplugin.SanitizeHeaderValue(headerName)
action, err := url.QueryUnescape(parts[1])
if err != nil {
return captureHeaders, fmt.Errorf("failed to decode percent encoding: %v", parts[1])
}
err = checkValidAction(action)
if err != nil {
return captureHeaders, err
}
capture = templateplugin.HTTPHeader{
Name: sanitizedHeaderName,
Action: routev1.RouteHTTPHeaderActionType(action),
}
captureHeaders = append(captureHeaders, capture)
}
}
}
}

return captureHeaders, err
}

func checkValidAction(action string) error {
if action != string(routev1.Set) && action != string(routev1.Delete) {
return fmt.Errorf("invalid action: %s", action)
} else {
return nil
}
}

// permittedHeaderNameRE is a compiled regexp to match an HTTP header name
// as specified in RFC 2616, section 4.2.
// Any changes made to regex of header name in route type in openshift/api and `validation.go` file in library-go must be reflected here and
// vice versa.
var permittedHeaderNameRE = regexp.MustCompile("^[-!#$%&'*+.0-9A-Z^_`a-z|~]+$")

// checkValidHeaderName verifies that the given HTTP header name is valid.
func checkValidHeaderName(headerName string) error {
if !permittedHeaderNameRE.MatchString(headerName) {
return fmt.Errorf("invalid HTTP header name: %s", headerName)
} else {
return nil
}
}

func parseCaptureCookie(in string) (*templateplugin.CaptureHTTPCookie, error) {
if len(in) == 0 {
return nil, nil
Expand Down Expand Up @@ -393,6 +499,22 @@ func (o *TemplateRouterOptions) Complete() error {
}
o.CaptureHTTPResponseHeaders = captureHTTPResponseHeaders

if len(o.HTTPResponseHeadersString) != 0 {
httpResponseHeaders, err := parseHeadersToBeSetOrDeleted(o.HTTPResponseHeadersString)
if err != nil {
return err
}
o.HTTPResponseHeaders = httpResponseHeaders
}

if len(o.HTTPRequestHeadersString) != 0 {
httpRequestHeaders, err := parseHeadersToBeSetOrDeleted(o.HTTPRequestHeadersString)
if err != nil {
return err
}
o.HTTPRequestHeaders = httpRequestHeaders
}

captureHTTPCookie, err := parseCaptureCookie(o.CaptureHTTPCookieString)
if err != nil {
return err
Expand Down Expand Up @@ -648,6 +770,8 @@ func (o *TemplateRouterOptions) Run(stopCh <-chan struct{}) error {
CaptureHTTPResponseHeaders: o.CaptureHTTPResponseHeaders,
CaptureHTTPCookie: o.CaptureHTTPCookie,
HTTPHeaderNameCaseAdjustments: o.HTTPHeaderNameCaseAdjustments,
HTTPResponseHeaders: o.HTTPResponseHeaders,
HTTPRequestHeaders: o.HTTPRequestHeaders,
}

svcFetcher := templateplugin.NewListWatchServiceLookup(kc.CoreV1(), o.ResyncInterval, o.Namespace)
Expand Down

0 comments on commit 44a5304

Please sign in to comment.