From 68dcebad273075d48421fe509698e167b50888b3 Mon Sep 17 00:00:00 2001 From: Audun Halland Date: Tue, 9 Oct 2018 15:37:04 +0200 Subject: [PATCH] Redirect unless https proto (#68) --- docs/usage.md | 86 +++++++++++++++++++++--------------------- proxy/ha_proxy_test.go | 66 ++++++++++++++++++++++++++++++++ proxy/template.go | 10 +++++ proxy/types.go | 2 + proxy/types_test.go | 2 + server/server_test.go | 6 ++- 6 files changed, 129 insertions(+), 43 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index ddb4d064..160c58e2 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -61,6 +61,7 @@ The following query parameters can be used only when `reqMode` is set to `http` |redirectFromDomain|If a request is sent to one of the domains in this list, it will be redirected to one of the values of the `serviceDomain`. Multiple domains can be separated with comma (e.g. `acme.com,something.acme.com`). The parameter can be prefixed with an index thus allowing definition of multiple destinations for a single service.
**Example:** `acme.com,something.acme.com`| |proxyInstanceName|When `FILTER_PROXY_INSTANCE_NAME` is set to `true`, only services with proxyInstanceName equal to `PROXY_INSTANCE_NAME` will be configured by this proxy.
**Example:** `docker-flow`| |redirectWhenHttpProto|Whether to redirect to https when X-Forwarded-Proto is set and the request is made over an HTTP port.
**Example:** `true`
**Default Value:** `false`| +|redirectUnlessHttpsProto|Whether to redirect to https unless X-Forwarded-Proto is explicitly `https`.
**Example:** `true`
**Default Value:** `false`| |serviceCert |Content of the PEM-encoded certificate to be used by the proxy when serving traffic over SSL.| |serviceDomain |The domain of the service. If set, the proxy will allow access only to requests coming to that domain. Multiple domains can be separated with comma (e.g. `acme.com,something.else.com`). The parameter can be prefixed with an index thus allowing definition of multiple destinations for a single service (e.g. `serviceDomain.1`, `serviceDomain.2`, and so on). Asterisk sign can be placed to beginning of value and in this case **serviceDomainAlgo** parameter will be **replaced** to `hdr_end(host)`. This parameter is **mandatory** if `servicePath` is not specified.
**Example:** `ecme.com`| |serviceDomainAlgo|Algorithm that should be applied to domain ACLs. Any ACL works only with one flag: `-i : ignore case during matching of all subsequent patterns`. If not set, the value of the environment variable `SERVICE_DOMAIN_ALGO` will be used instead. If defaults to `hdr_beg(host)`
**Examples:**
`hdr(host)`: matches only if domain is the same as `serviceDomain`
`hdr_dom(host)`: matches the specified `serviceDomain` and any subdomain (a string either isolated or delimited by dots). **Example:** if `hdr_dom(host)` contains `www.ecme.com` and `serviceDomain` equals `ecme.com` the rule will be passed.
`req.ssl_sni`: matches Server Name TLS extension| @@ -123,48 +124,49 @@ The environment variables must apply the rules that follow. The map between the HTTP query parameters and environment variables is as follows. -|Query |Environment variable | -|---------------------|------------------------| -|aclName |ACL_NAME | -|addReqHeader |ADD_REQ_HEADER | -|addResHeader |ADD_RES_HEADER | -|allowedMethods |ALLOWED_METHODS | -|backendExtra |BACKEND_EXTRA | -|compressionAlgo |COMPRESSION_ALGO | -|compressionType |COMPRESSION_TYPE | -|deniedMethods |DENIED_METHODS | -|denyHttp |DENY_HTTP | -|distribute |DISTRIBUTE | -|httpsOnly |HTTPS_ONLY | -|httpsPort |HTTPS_PORT | -|ignoreAuthorization |IGNORE_AUTHORIZATION | -|isDefaultBackend |IS_DEFAULT_BACKEND | -|outboundHostname |OUTBOUND_HOSTNAME | -|pathType |PATH_TYPE | -|port |PORT | -|redirectFromDomain |REDIRECT_FROM_DOMAIN | -|redirectWhenHttpProto|REDIRECT_WHEN_HTTP_PROTO| -|reqMode |REQ_MODE | -|reqPathSearchReplace |REQ_PATH_SEARCH_REPLACE | -|serviceCert |SERVICE_CERT | -|serviceDomain |SERVICE_DOMAIN | -|serviceName |SERVICE_NAME | -|servicePath |SERVICE_PATH | -|servicePathExclude |SERVICE_PATH_EXCLUDE | -|setReqHeader |SET_REQ_HEADER | -|setResHeader |SET_RES_HEADER | -|srcPort |SRC_PORT | -|srcHttpsPort |SRC_HTTPS_PORT | -|sslVerifyNone |SSL_VERIFY_NONE | -|templateBePath |TEMPLATE_BE_PATH | -|templateFePath |TEMPLATE_FE_PATH | -|timeoutServer |TIMEOUT_SERVER | -|timeoutClient |TIMEOUT_CLIENT | -|timeoutTunnel |TIMEOUT_TUNNEL | -|users       |**Not supported** | -|usersSecret |**Not supported** | -|usersPassEncrypted |**Not supported** | -|verifyClientSsl |VERIFY_CLIENT_SSL | +|Query |Environment variable | +|------------------------|---------------------------| +|aclName |ACL_NAME | +|addReqHeader |ADD_REQ_HEADER | +|addResHeader |ADD_RES_HEADER | +|allowedMethods |ALLOWED_METHODS | +|backendExtra |BACKEND_EXTRA | +|compressionAlgo |COMPRESSION_ALGO | +|compressionType |COMPRESSION_TYPE | +|deniedMethods |DENIED_METHODS | +|denyHttp |DENY_HTTP | +|distribute |DISTRIBUTE | +|httpsOnly |HTTPS_ONLY | +|httpsPort |HTTPS_PORT | +|ignoreAuthorization |IGNORE_AUTHORIZATION | +|isDefaultBackend |IS_DEFAULT_BACKEND | +|outboundHostname |OUTBOUND_HOSTNAME | +|pathType |PATH_TYPE | +|port |PORT | +|redirectFromDomain |REDIRECT_FROM_DOMAIN | +|redirectWhenHttpProto |REDIRECT_WHEN_HTTP_PROTO | +|redirectUnlessHttpsProto|REDIRECT_UNLESS_HTTPS_PROTO| +|reqMode |REQ_MODE | +|reqPathSearchReplace |REQ_PATH_SEARCH_REPLACE | +|serviceCert |SERVICE_CERT | +|serviceDomain |SERVICE_DOMAIN | +|serviceName |SERVICE_NAME | +|servicePath |SERVICE_PATH | +|servicePathExclude |SERVICE_PATH_EXCLUDE | +|setReqHeader |SET_REQ_HEADER | +|setResHeader |SET_RES_HEADER | +|srcPort |SRC_PORT | +|srcHttpsPort |SRC_HTTPS_PORT | +|sslVerifyNone |SSL_VERIFY_NONE | +|templateBePath |TEMPLATE_BE_PATH | +|templateFePath |TEMPLATE_FE_PATH | +|timeoutServer |TIMEOUT_SERVER | +|timeoutClient |TIMEOUT_CLIENT | +|timeoutTunnel |TIMEOUT_TUNNEL | +|users       |**Not supported** | +|usersSecret |**Not supported** | +|usersPassEncrypted |**Not supported** | +|verifyClientSsl |VERIFY_CLIENT_SSL | Please explore the [Configuring Non-Swarm Services](non-swarm.md) tutorial for more info. diff --git a/proxy/ha_proxy_test.go b/proxy/ha_proxy_test.go index 7cdd2cb5..82b7c820 100644 --- a/proxy/ha_proxy_test.go +++ b/proxy/ha_proxy_test.go @@ -2133,6 +2133,72 @@ func (s HaProxyTestSuite) Test_CreateConfigFromTemplates_usesHttpsRedirectCode() s.Equal(expectedData, actualData) } +func (s HaProxyTestSuite) Test_CreateConfigFromTemplates_ForwardsToHttps_WhenRedirectUnlessHttpsProtoIsTrue() { + var actualData string + tmpl := s.TemplateContent + expectedData := fmt.Sprintf( + `%s + acl url_my-service1111_0 path_beg /path + acl domain_my-service1111_0 hdr_beg(host) -i my-domain.com + acl is_my-service_https hdr(X-Forwarded-Proto) https + http-request redirect scheme https if !is_my-service_https url_my-service1111_0 domain_my-service1111_0 + use_backend my-service-be1111_0 if url_my-service1111_0 domain_my-service1111_0%s`, + tmpl, + s.ServicesContent, + ) + writeFile = func(filename string, data []byte, perm os.FileMode) error { + actualData = string(data) + return nil + } + p := NewHaProxy(s.TemplatesPath, s.ConfigsPath) + service1 := Service{ + ServiceName: "my-service", + RedirectUnlessHttpsProto: true, + AclName: "my-service", + ServiceDest: []ServiceDest{ + {Port: "1111", ServicePath: []string{"/path"}, ServiceDomain: []string{"my-domain.com"}, PathType: "path_beg"}, + }, + } + p.AddService(service1) + + p.CreateConfigFromTemplates() + + s.Equal(expectedData, actualData) +} + +func (s HaProxyTestSuite) Test_CreateConfigFromTemplates_usesHttpsRedirectCode_3WhenRedirectUnlessHttpsProtoIsTrue() { + var actualData string + tmpl := s.TemplateContent + expectedData := fmt.Sprintf( + `%s + acl url_my-service1111_0 path_beg /path + acl domain_my-service1111_0 hdr_beg(host) -i my-domain.com + acl is_my-service_https hdr(X-Forwarded-Proto) https + http-request redirect scheme https code 301 if !is_my-service_https url_my-service1111_0 domain_my-service1111_0 + use_backend my-service-be1111_0 if url_my-service1111_0 domain_my-service1111_0%s`, + tmpl, + s.ServicesContent, + ) + writeFile = func(filename string, data []byte, perm os.FileMode) error { + actualData = string(data) + return nil + } + p := NewHaProxy(s.TemplatesPath, s.ConfigsPath) + service1 := Service{ + ServiceName: "my-service", + RedirectUnlessHttpsProto: true, + AclName: "my-service", + ServiceDest: []ServiceDest{ + {Port: "1111", ServicePath: []string{"/path"}, ServiceDomain: []string{"my-domain.com"}, HttpsRedirectCode: "301", PathType: "path_beg"}, + }, + } + p.AddService(service1) + + p.CreateConfigFromTemplates() + + s.Equal(expectedData, actualData) +} + func (s HaProxyTestSuite) Test_CreateConfigFromTemplates_ForwardsToDomain_WhenRedirectFromDomainIsSet() { var actualData string tmpl := s.TemplateContent diff --git a/proxy/template.go b/proxy/template.go index 8a8960cc..04939bee 100644 --- a/proxy/template.go +++ b/proxy/template.go @@ -66,6 +66,16 @@ func getFrontTemplate(s Service) string { {{- end}} {{- end}} {{- end}} +{{- if $.RedirectUnlessHttpsProto}} + {{- range .ServiceDest}} + {{- if eq .ReqMode "http"}} + {{- if ne .Port ""}} + acl is_{{$.AclName}}_https hdr(X-Forwarded-Proto) https + http-request redirect scheme https{{if .HttpsRedirectCode}} code {{.HttpsRedirectCode}}{{end}} if !is_{{$.AclName}}_https url_{{$.AclName}}{{.Port}}_{{.Index}}{{if .ServiceDomain}} domain_{{$.AclName}}{{.Port}}_{{.Index}}{{end}}{{.SrcPortAclName}} + {{- end}} + {{- end}} + {{- end}} +{{- end}} {{- range $sd := .ServiceDest}} {{- if eq .ReqMode "http" }} {{- if ne .Port ""}} diff --git a/proxy/types.go b/proxy/types.go index 9af6c63b..67fabd60 100644 --- a/proxy/types.go +++ b/proxy/types.go @@ -166,6 +166,8 @@ type Service struct { ProxyInstanceName string `split_words:"true"` // Whether to redirect to https when X-Forwarded-Proto is http RedirectWhenHttpProto bool `split_words:"true"` + // Whether to redirect to https unless X-Forwarded-Proto is https + RedirectUnlessHttpsProto bool `split_words:"true"` // The number of replicas of a service. // This parameter is used if `DiscoveryType` is set to `DNS`. // Non-Global services with 0 replicas will not be added to the HAproxy config. diff --git a/proxy/types_test.go b/proxy/types_test.go index b4c55f3c..d5e66dba 100644 --- a/proxy/types_test.go +++ b/proxy/types_test.go @@ -381,6 +381,7 @@ func (s *TypesTestSuite) getServiceMap(expected Service, indexSuffix, separator "isDefaultBackend": strconv.FormatBool(expected.IsDefaultBackend), "proxyInstanceName": expected.ProxyInstanceName, "redirectWhenHttpProto": strconv.FormatBool(expected.RedirectWhenHttpProto), + "redirectUnlessHttpsProto": strconv.FormatBool(expected.RedirectUnlessHttpsProto), "reqPathReplace": expected.ReqPathReplace, "reqPathSearch": expected.ReqPathSearch, "replicas": strconv.Itoa(expected.Replicas), @@ -438,6 +439,7 @@ func (s *TypesTestSuite) getExpectedService() Service { IsDefaultBackend: true, ProxyInstanceName: "docker-flow", RedirectWhenHttpProto: true, + RedirectUnlessHttpsProto: true, Replicas: 3, ReqPathReplace: "reqPathReplace", ReqPathSearch: "reqPathSearch", diff --git a/server/server_test.go b/server/server_test.go index a4f7ae9c..e8aa3ba0 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -589,6 +589,7 @@ func (s *ServerTestSuite) Test_GetServiceFromUrl_ReturnsProxyService() { Distribute: true, ProxyInstanceName: "docker-flow", RedirectWhenHttpProto: true, + RedirectUnlessHttpsProto: true, Replicas: 83, ReqPathReplace: "reqPathReplace", ReqPathSearch: "reqPathSearch", @@ -629,7 +630,7 @@ func (s *ServerTestSuite) Test_GetServiceFromUrl_ReturnsProxyService() { {Username: "user2", Password: "pass2", PassEncrypted: true}}, } addr := fmt.Sprintf( - "%s?serviceName=%s&users=%s&usersPassEncrypted=%t&aclName=%s&balanceGroup=%s&checkTcp=%t&clitcpka=%t&serviceCert=%s&outboundHostname=%s&pathType=%s&proxyInstanceName=%s&reqPathSearch=%s&reqPathReplace=%s&templateFePath=%s&templateBePath=%s&timeoutServer=%s&timeoutClient=%s&timeoutTunnel=%s&reqMode=%s&httpsOnly=%t&httpsRedirectCode=%s&isDefaultBackend=%t&redirectWhenHttpProto=%t&httpsPort=%d&srcPort=%d&srcHttpsPort=%d&serviceDomain=%s&redirectFromDomain=%s&distribute=%t&sslVerifyNone=%t&serviceDomainAlgo=%s&addReqHeader=%s&addResHeader=%s&setReqHeader=%s&setResHeader=%s&delReqHeader=%s&delResHeader=%s&servicePath=/&servicePathExclude=%s&port=1234&connectionMode=%s&serviceHeader=X-Version:3,name:Viktor&allowedMethods=GET,DELETE&deniedMethods=PUT,POST&compressionAlgo=%s&compressionType=%s&checkResolvers=%t&replicas=%d&discoveryType=%s", + "%s?serviceName=%s&users=%s&usersPassEncrypted=%t&aclName=%s&balanceGroup=%s&checkTcp=%t&clitcpka=%t&serviceCert=%s&outboundHostname=%s&pathType=%s&proxyInstanceName=%s&reqPathSearch=%s&reqPathReplace=%s&templateFePath=%s&templateBePath=%s&timeoutServer=%s&timeoutClient=%s&timeoutTunnel=%s&reqMode=%s&httpsOnly=%t&httpsRedirectCode=%s&isDefaultBackend=%t&redirectWhenHttpProto=%t&redirectUnlessHttpsProto=%t&httpsPort=%d&srcPort=%d&srcHttpsPort=%d&serviceDomain=%s&redirectFromDomain=%s&distribute=%t&sslVerifyNone=%t&serviceDomainAlgo=%s&addReqHeader=%s&addResHeader=%s&setReqHeader=%s&setResHeader=%s&delReqHeader=%s&delResHeader=%s&servicePath=/&servicePathExclude=%s&port=1234&connectionMode=%s&serviceHeader=X-Version:3,name:Viktor&allowedMethods=GET,DELETE&deniedMethods=PUT,POST&compressionAlgo=%s&compressionType=%s&checkResolvers=%t&replicas=%d&discoveryType=%s", s.BaseUrl, expected.ServiceName, @@ -655,6 +656,7 @@ func (s *ServerTestSuite) Test_GetServiceFromUrl_ReturnsProxyService() { expected.ServiceDest[0].HttpsRedirectCode, expected.IsDefaultBackend, expected.RedirectWhenHttpProto, + expected.RedirectUnlessHttpsProto, expected.ServiceDest[0].HttpsPort, expected.ServiceDest[0].SrcPort, expected.ServiceDest[0].SrcHttpsPort, @@ -843,6 +845,7 @@ func (s *ServerTestSuite) Test_GetServicesFromEnvVars_ReturnsServices() { os.Setenv("DFP_SERVICE_PATH_TYPE", service.ServiceDest[0].PathType) os.Setenv("DFP_SERVICE_REDIRECT_FROM_DOMAIN", strings.Join(service.ServiceDest[0].RedirectFromDomain, ",")) os.Setenv("DFP_SERVICE_REDIRECT_WHEN_HTTP_PROTO", strconv.FormatBool(service.RedirectWhenHttpProto)) + os.Setenv("DFP_SERVICE_REDIRECT_UNLESS_HTTPS_PROTO", strconv.FormatBool(service.RedirectUnlessHttpsProto)) os.Setenv("DFP_SERVICE_REQ_MODE", service.ServiceDest[0].ReqMode) os.Setenv("DFP_SERVICE_REQ_PATH_SEARCH_REPLACE", service.ServiceDest[0].ReqPathSearchReplace) os.Setenv("DFP_SERVICE_SERVICE_CERT", service.ServiceCert) @@ -888,6 +891,7 @@ func (s *ServerTestSuite) Test_GetServicesFromEnvVars_ReturnsServices() { os.Unsetenv("DFP_SERVICE_PORT") os.Unsetenv("DFP_SERVICE_REDIRECT_FROM_DOMAIN") os.Unsetenv("DFP_SERVICE_REDIRECT_WHEN_HTTP_PROTO") + os.Unsetenv("DFP_SERVICE_REDIRECT_UNLESS_HTTPS_PROTO") os.Unsetenv("DFP_SERVICE_REQ_MODE") os.Unsetenv("DFP_SERVICE_REQ_PATH_SEARCH_REPLACE") os.Unsetenv("DFP_SERVICE_SERVICE_CERT")