Skip to content

Commit 68dceba

Browse files
audunhallandthomasjpfan
authored andcommitted
Redirect unless https proto (#68)
1 parent 982aaaa commit 68dceba

6 files changed

Lines changed: 129 additions & 43 deletions

File tree

docs/usage.md

Lines changed: 44 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ The following query parameters can be used only when `reqMode` is set to `http`
6161
|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.<br>**Example:** `acme.com,something.acme.com`|
6262
|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.<br>**Example:** `docker-flow`|
6363
|redirectWhenHttpProto|Whether to redirect to https when X-Forwarded-Proto is set and the request is made over an HTTP port.<br>**Example:** `true`<br>**Default Value:** `false`|
64+
|redirectUnlessHttpsProto|Whether to redirect to https unless X-Forwarded-Proto is explicitly `https`.<br>**Example:** `true`<br>**Default Value:** `false`|
6465
|serviceCert |Content of the PEM-encoded certificate to be used by the proxy when serving traffic over SSL.|
6566
|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.<br>**Example:** `ecme.com`|
6667
|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)`<br>**Examples:**<br>`hdr(host)`: matches only if domain is the same as `serviceDomain`<br>`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.<br>`req.ssl_sni`: matches Server Name TLS extension|
@@ -123,48 +124,49 @@ The environment variables must apply the rules that follow.
123124

124125
The map between the HTTP query parameters and environment variables is as follows.
125126

126-
|Query |Environment variable |
127-
|---------------------|------------------------|
128-
|aclName |ACL_NAME |
129-
|addReqHeader |ADD_REQ_HEADER |
130-
|addResHeader |ADD_RES_HEADER |
131-
|allowedMethods |ALLOWED_METHODS |
132-
|backendExtra |BACKEND_EXTRA |
133-
|compressionAlgo |COMPRESSION_ALGO |
134-
|compressionType |COMPRESSION_TYPE |
135-
|deniedMethods |DENIED_METHODS |
136-
|denyHttp |DENY_HTTP |
137-
|distribute |DISTRIBUTE |
138-
|httpsOnly |HTTPS_ONLY |
139-
|httpsPort |HTTPS_PORT |
140-
|ignoreAuthorization |IGNORE_AUTHORIZATION |
141-
|isDefaultBackend |IS_DEFAULT_BACKEND |
142-
|outboundHostname |OUTBOUND_HOSTNAME |
143-
|pathType |PATH_TYPE |
144-
|port |PORT |
145-
|redirectFromDomain |REDIRECT_FROM_DOMAIN |
146-
|redirectWhenHttpProto|REDIRECT_WHEN_HTTP_PROTO|
147-
|reqMode |REQ_MODE |
148-
|reqPathSearchReplace |REQ_PATH_SEARCH_REPLACE |
149-
|serviceCert |SERVICE_CERT |
150-
|serviceDomain |SERVICE_DOMAIN |
151-
|serviceName |SERVICE_NAME |
152-
|servicePath |SERVICE_PATH |
153-
|servicePathExclude |SERVICE_PATH_EXCLUDE |
154-
|setReqHeader |SET_REQ_HEADER |
155-
|setResHeader |SET_RES_HEADER |
156-
|srcPort |SRC_PORT |
157-
|srcHttpsPort |SRC_HTTPS_PORT |
158-
|sslVerifyNone |SSL_VERIFY_NONE |
159-
|templateBePath |TEMPLATE_BE_PATH |
160-
|templateFePath |TEMPLATE_FE_PATH |
161-
|timeoutServer |TIMEOUT_SERVER |
162-
|timeoutClient |TIMEOUT_CLIENT |
163-
|timeoutTunnel |TIMEOUT_TUNNEL |
164-
|users       |**Not supported** |
165-
|usersSecret |**Not supported** |
166-
|usersPassEncrypted |**Not supported** |
167-
|verifyClientSsl |VERIFY_CLIENT_SSL |
127+
|Query |Environment variable |
128+
|------------------------|---------------------------|
129+
|aclName |ACL_NAME |
130+
|addReqHeader |ADD_REQ_HEADER |
131+
|addResHeader |ADD_RES_HEADER |
132+
|allowedMethods |ALLOWED_METHODS |
133+
|backendExtra |BACKEND_EXTRA |
134+
|compressionAlgo |COMPRESSION_ALGO |
135+
|compressionType |COMPRESSION_TYPE |
136+
|deniedMethods |DENIED_METHODS |
137+
|denyHttp |DENY_HTTP |
138+
|distribute |DISTRIBUTE |
139+
|httpsOnly |HTTPS_ONLY |
140+
|httpsPort |HTTPS_PORT |
141+
|ignoreAuthorization |IGNORE_AUTHORIZATION |
142+
|isDefaultBackend |IS_DEFAULT_BACKEND |
143+
|outboundHostname |OUTBOUND_HOSTNAME |
144+
|pathType |PATH_TYPE |
145+
|port |PORT |
146+
|redirectFromDomain |REDIRECT_FROM_DOMAIN |
147+
|redirectWhenHttpProto |REDIRECT_WHEN_HTTP_PROTO |
148+
|redirectUnlessHttpsProto|REDIRECT_UNLESS_HTTPS_PROTO|
149+
|reqMode |REQ_MODE |
150+
|reqPathSearchReplace |REQ_PATH_SEARCH_REPLACE |
151+
|serviceCert |SERVICE_CERT |
152+
|serviceDomain |SERVICE_DOMAIN |
153+
|serviceName |SERVICE_NAME |
154+
|servicePath |SERVICE_PATH |
155+
|servicePathExclude |SERVICE_PATH_EXCLUDE |
156+
|setReqHeader |SET_REQ_HEADER |
157+
|setResHeader |SET_RES_HEADER |
158+
|srcPort |SRC_PORT |
159+
|srcHttpsPort |SRC_HTTPS_PORT |
160+
|sslVerifyNone |SSL_VERIFY_NONE |
161+
|templateBePath |TEMPLATE_BE_PATH |
162+
|templateFePath |TEMPLATE_FE_PATH |
163+
|timeoutServer |TIMEOUT_SERVER |
164+
|timeoutClient |TIMEOUT_CLIENT |
165+
|timeoutTunnel |TIMEOUT_TUNNEL |
166+
|users       |**Not supported** |
167+
|usersSecret |**Not supported** |
168+
|usersPassEncrypted |**Not supported** |
169+
|verifyClientSsl |VERIFY_CLIENT_SSL |
168170

169171
Please explore the [Configuring Non-Swarm Services](non-swarm.md) tutorial for more info.
170172

proxy/ha_proxy_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2133,6 +2133,72 @@ func (s HaProxyTestSuite) Test_CreateConfigFromTemplates_usesHttpsRedirectCode()
21332133
s.Equal(expectedData, actualData)
21342134
}
21352135

2136+
func (s HaProxyTestSuite) Test_CreateConfigFromTemplates_ForwardsToHttps_WhenRedirectUnlessHttpsProtoIsTrue() {
2137+
var actualData string
2138+
tmpl := s.TemplateContent
2139+
expectedData := fmt.Sprintf(
2140+
`%s
2141+
acl url_my-service1111_0 path_beg /path
2142+
acl domain_my-service1111_0 hdr_beg(host) -i my-domain.com
2143+
acl is_my-service_https hdr(X-Forwarded-Proto) https
2144+
http-request redirect scheme https if !is_my-service_https url_my-service1111_0 domain_my-service1111_0
2145+
use_backend my-service-be1111_0 if url_my-service1111_0 domain_my-service1111_0%s`,
2146+
tmpl,
2147+
s.ServicesContent,
2148+
)
2149+
writeFile = func(filename string, data []byte, perm os.FileMode) error {
2150+
actualData = string(data)
2151+
return nil
2152+
}
2153+
p := NewHaProxy(s.TemplatesPath, s.ConfigsPath)
2154+
service1 := Service{
2155+
ServiceName: "my-service",
2156+
RedirectUnlessHttpsProto: true,
2157+
AclName: "my-service",
2158+
ServiceDest: []ServiceDest{
2159+
{Port: "1111", ServicePath: []string{"/path"}, ServiceDomain: []string{"my-domain.com"}, PathType: "path_beg"},
2160+
},
2161+
}
2162+
p.AddService(service1)
2163+
2164+
p.CreateConfigFromTemplates()
2165+
2166+
s.Equal(expectedData, actualData)
2167+
}
2168+
2169+
func (s HaProxyTestSuite) Test_CreateConfigFromTemplates_usesHttpsRedirectCode_3WhenRedirectUnlessHttpsProtoIsTrue() {
2170+
var actualData string
2171+
tmpl := s.TemplateContent
2172+
expectedData := fmt.Sprintf(
2173+
`%s
2174+
acl url_my-service1111_0 path_beg /path
2175+
acl domain_my-service1111_0 hdr_beg(host) -i my-domain.com
2176+
acl is_my-service_https hdr(X-Forwarded-Proto) https
2177+
http-request redirect scheme https code 301 if !is_my-service_https url_my-service1111_0 domain_my-service1111_0
2178+
use_backend my-service-be1111_0 if url_my-service1111_0 domain_my-service1111_0%s`,
2179+
tmpl,
2180+
s.ServicesContent,
2181+
)
2182+
writeFile = func(filename string, data []byte, perm os.FileMode) error {
2183+
actualData = string(data)
2184+
return nil
2185+
}
2186+
p := NewHaProxy(s.TemplatesPath, s.ConfigsPath)
2187+
service1 := Service{
2188+
ServiceName: "my-service",
2189+
RedirectUnlessHttpsProto: true,
2190+
AclName: "my-service",
2191+
ServiceDest: []ServiceDest{
2192+
{Port: "1111", ServicePath: []string{"/path"}, ServiceDomain: []string{"my-domain.com"}, HttpsRedirectCode: "301", PathType: "path_beg"},
2193+
},
2194+
}
2195+
p.AddService(service1)
2196+
2197+
p.CreateConfigFromTemplates()
2198+
2199+
s.Equal(expectedData, actualData)
2200+
}
2201+
21362202
func (s HaProxyTestSuite) Test_CreateConfigFromTemplates_ForwardsToDomain_WhenRedirectFromDomainIsSet() {
21372203
var actualData string
21382204
tmpl := s.TemplateContent

proxy/template.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,16 @@ func getFrontTemplate(s Service) string {
6666
{{- end}}
6767
{{- end}}
6868
{{- end}}
69+
{{- if $.RedirectUnlessHttpsProto}}
70+
{{- range .ServiceDest}}
71+
{{- if eq .ReqMode "http"}}
72+
{{- if ne .Port ""}}
73+
acl is_{{$.AclName}}_https hdr(X-Forwarded-Proto) https
74+
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}}
75+
{{- end}}
76+
{{- end}}
77+
{{- end}}
78+
{{- end}}
6979
{{- range $sd := .ServiceDest}}
7080
{{- if eq .ReqMode "http" }}
7181
{{- if ne .Port ""}}

proxy/types.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,8 @@ type Service struct {
166166
ProxyInstanceName string `split_words:"true"`
167167
// Whether to redirect to https when X-Forwarded-Proto is http
168168
RedirectWhenHttpProto bool `split_words:"true"`
169+
// Whether to redirect to https unless X-Forwarded-Proto is https
170+
RedirectUnlessHttpsProto bool `split_words:"true"`
169171
// The number of replicas of a service.
170172
// This parameter is used if `DiscoveryType` is set to `DNS`.
171173
// Non-Global services with 0 replicas will not be added to the HAproxy config.

proxy/types_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,7 @@ func (s *TypesTestSuite) getServiceMap(expected Service, indexSuffix, separator
381381
"isDefaultBackend": strconv.FormatBool(expected.IsDefaultBackend),
382382
"proxyInstanceName": expected.ProxyInstanceName,
383383
"redirectWhenHttpProto": strconv.FormatBool(expected.RedirectWhenHttpProto),
384+
"redirectUnlessHttpsProto": strconv.FormatBool(expected.RedirectUnlessHttpsProto),
384385
"reqPathReplace": expected.ReqPathReplace,
385386
"reqPathSearch": expected.ReqPathSearch,
386387
"replicas": strconv.Itoa(expected.Replicas),
@@ -438,6 +439,7 @@ func (s *TypesTestSuite) getExpectedService() Service {
438439
IsDefaultBackend: true,
439440
ProxyInstanceName: "docker-flow",
440441
RedirectWhenHttpProto: true,
442+
RedirectUnlessHttpsProto: true,
441443
Replicas: 3,
442444
ReqPathReplace: "reqPathReplace",
443445
ReqPathSearch: "reqPathSearch",

server/server_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,7 @@ func (s *ServerTestSuite) Test_GetServiceFromUrl_ReturnsProxyService() {
589589
Distribute: true,
590590
ProxyInstanceName: "docker-flow",
591591
RedirectWhenHttpProto: true,
592+
RedirectUnlessHttpsProto: true,
592593
Replicas: 83,
593594
ReqPathReplace: "reqPathReplace",
594595
ReqPathSearch: "reqPathSearch",
@@ -629,7 +630,7 @@ func (s *ServerTestSuite) Test_GetServiceFromUrl_ReturnsProxyService() {
629630
{Username: "user2", Password: "pass2", PassEncrypted: true}},
630631
}
631632
addr := fmt.Sprintf(
632-
"%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",
633+
"%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",
633634

634635
s.BaseUrl,
635636
expected.ServiceName,
@@ -655,6 +656,7 @@ func (s *ServerTestSuite) Test_GetServiceFromUrl_ReturnsProxyService() {
655656
expected.ServiceDest[0].HttpsRedirectCode,
656657
expected.IsDefaultBackend,
657658
expected.RedirectWhenHttpProto,
659+
expected.RedirectUnlessHttpsProto,
658660
expected.ServiceDest[0].HttpsPort,
659661
expected.ServiceDest[0].SrcPort,
660662
expected.ServiceDest[0].SrcHttpsPort,
@@ -843,6 +845,7 @@ func (s *ServerTestSuite) Test_GetServicesFromEnvVars_ReturnsServices() {
843845
os.Setenv("DFP_SERVICE_PATH_TYPE", service.ServiceDest[0].PathType)
844846
os.Setenv("DFP_SERVICE_REDIRECT_FROM_DOMAIN", strings.Join(service.ServiceDest[0].RedirectFromDomain, ","))
845847
os.Setenv("DFP_SERVICE_REDIRECT_WHEN_HTTP_PROTO", strconv.FormatBool(service.RedirectWhenHttpProto))
848+
os.Setenv("DFP_SERVICE_REDIRECT_UNLESS_HTTPS_PROTO", strconv.FormatBool(service.RedirectUnlessHttpsProto))
846849
os.Setenv("DFP_SERVICE_REQ_MODE", service.ServiceDest[0].ReqMode)
847850
os.Setenv("DFP_SERVICE_REQ_PATH_SEARCH_REPLACE", service.ServiceDest[0].ReqPathSearchReplace)
848851
os.Setenv("DFP_SERVICE_SERVICE_CERT", service.ServiceCert)
@@ -888,6 +891,7 @@ func (s *ServerTestSuite) Test_GetServicesFromEnvVars_ReturnsServices() {
888891
os.Unsetenv("DFP_SERVICE_PORT")
889892
os.Unsetenv("DFP_SERVICE_REDIRECT_FROM_DOMAIN")
890893
os.Unsetenv("DFP_SERVICE_REDIRECT_WHEN_HTTP_PROTO")
894+
os.Unsetenv("DFP_SERVICE_REDIRECT_UNLESS_HTTPS_PROTO")
891895
os.Unsetenv("DFP_SERVICE_REQ_MODE")
892896
os.Unsetenv("DFP_SERVICE_REQ_PATH_SEARCH_REPLACE")
893897
os.Unsetenv("DFP_SERVICE_SERVICE_CERT")

0 commit comments

Comments
 (0)