diff --git a/actions/fetch_test.go b/actions/fetch_test.go index f9600abd..b362ee60 100644 --- a/actions/fetch_test.go +++ b/actions/fetch_test.go @@ -160,7 +160,6 @@ func (s *FetchTestSuite) Test_ReloadConfig_SendsARequestToSwarmListener_WhenList reconfigureMock.AssertNumberOfCalls(s.T(), "Execute", 1) proxyMock.AssertCalled(s.T(), "Reload") proxyMock.AssertCalled(s.T(), "CreateConfigFromTemplates") - } func (s *FetchTestSuite) Test_ReloadConfig_ReturnsError_WhenSwarmListenerReturnsWrongData() { diff --git a/actions/reconfigure_test.go b/actions/reconfigure_test.go index d9daf143..e7f5f26f 100644 --- a/actions/reconfigure_test.go +++ b/actions/reconfigure_test.go @@ -510,7 +510,7 @@ func (s ReconfigureTestSuite) Test_Execute_WritesBeTemplateWithRedirectToHttps_W ` backend %s-be%s_0 mode http - redirect scheme https if !{ ssl_fc } + http-request redirect scheme https if !{ ssl_fc } server %s %s:%s`, s.ServiceName, s.reconfigure.ServiceDest[0].Port, diff --git a/docs/usage.md b/docs/usage.md index 88657049..92c48485 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -37,7 +37,7 @@ The following query parameters can be used to send a *reconfigure* request to *D |timeoutTunnel |The tunnel timeout in seconds.
**Default:** `3600`
**Example:** `3600`| |xForwardedProto|Whether to add "X-Forwarded-Proto https" header.
**Default:** `false`
**Example:** `true`| -Multiple destinations for a single service can be specified by adding index as a suffix to `servicePath`, `srcPort`, `port`, `userAgent`, `ignoreAuthorization`, `serviceDomain``allowedMethods`, `deniedMethods`, `denyHttp`, `httpsOnly`, or `ReqMode` parameters. In that case, `srcPort` is required. +Multiple destinations for a single service can be specified by adding index as a suffix to `servicePath`, `srcPort`, `port`, `userAgent`, `ignoreAuthorization`, `serviceDomain``allowedMethods`, `deniedMethods`, `denyHttp`, `httpsOnly`, `redirectFromDomain`, or `ReqMode` parameters. In that case, `srcPort` is required. ### HTTP Mode Query Parameters @@ -51,9 +51,10 @@ The following query parameters can be used only when `reqMode` is set to `http` |httpsOnly |If set to true, HTTP requests to the service will be redirected to HTTPS. The parameter can be prefixed with an index thus allowing definition of multiple destinations for a single service (e.g. `httpsOnly.1`, `httpsOnly.2`, and so on).
**Example:** `true`
**Default Value:** `false`| |outboundHostname|The hostname where the service is running, for instance on a separate swarm. If specified, the proxy will dispatch requests to that domain.
**Example:** `ecme.com`| |pathType |The ACL derivative. Defaults to *path_beg*. See [HAProxy path](https://cbonte.github.io/haproxy-dconv/configuration-1.5.html#7.3.6-path) for more info.
**Example:** `path_beg`| +|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`| |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`| |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. 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 begining 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`| +|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(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| |serviceHeader|Headers used to filter requests. If set, the proxy will allow access only to requests that contain specified headers. A header consists of a key and value separated with colon (e.g. `X-Version:3`). Multiple headers can be separated with comma (e.g. `X-Version:3,name:viktor`). The parameter can be prefixed with an index thus allowing definition of multiple destinations for a single service (e.g. `serviceHeader.1`, `serviceHeader.2`, and so on).
**Example:** `X-Version:3,name:viktor`| |servicePath |The URL path of the service. Multiple values should be separated with comma (`,`). The parameter can be prefixed with an index thus allowing definition of multiple destinations for a single service (e.g. `servicePath.1`, `servicePath.2`, and so on). This parameter **is mandatory** unless `serviceDomain` is specified.
**Example:** `/api/v1/books`| @@ -67,7 +68,7 @@ The following query parameters can be used only when `reqMode` is set to `http` |usersPassEncrypted|Indicates whether passwords provided by `users` or `usersSecret` contain encrypted data. Passwords can be encrypted with the command `mkpasswd -m sha-512 password1`.
**Example:** `true`
**Default Value:** `false`| |verifyClientSsl|Whether to verify client SSL and, if it is not valid, deny request and return 403 Forbidden status code. SSL is validated against the `ca-file` specified through the environment variable `CA_FILE`.
**Example:** true
**Default Value:** `false`| -Multiple destinations for a single service can be specified by adding index as a suffix to `servicePath`, `srcPort`, `port`, `userAgent`, `ignoreAuthorization`, `serviceDomain`, `allowedMethods`, `deniedMethods`, `denyHttp`, `httpsOnly`, or `ReqMode` parameters. In that case, `srcPort` is required. +Multiple destinations for a single service can be specified by adding index as a suffix to `servicePath`, `srcPort`, `port`, `userAgent`, `ignoreAuthorization`, `serviceDomain`, `allowedMethods`, `deniedMethods`, `denyHttp`, `httpsOnly`, `redirectFromDomain`, or `ReqMode` parameters. In that case, `srcPort` is required. ### TCP Mode HTTP Query Parameters @@ -121,6 +122,7 @@ The map between the HTTP query parameters and environment variables is as follow |outboundHostname |OUTBOUND_HOSTNAME | |pathType |PATH_TYPE | |port |PORT | +|redirectFromDomain |REDIRECT_FROM_DOMAIN | |redirectWhenHttpProto|REDIRECT_WHEN_HTTP_PROTO| |reqMode |REQ_MODE | |reqPathReplace |REQ_PATH_REPLACE | diff --git a/integration_tests/integration_swarm_test.go b/integration_tests/integration_swarm_test.go index 87294b36..da6dd276 100644 --- a/integration_tests/integration_swarm_test.go +++ b/integration_tests/integration_swarm_test.go @@ -105,6 +105,23 @@ func (s IntegrationSwarmTestSuite) Test_Domain() { } } +func (s IntegrationSwarmTestSuite) Test_RedirectFromDomain() { + params := fmt.Sprintf("&serviceDomain=%s&redirectFromDomain=my-other-domain.com", s.hostIP) + s.reconfigureGoDemo(params) + + client := new(http.Client) + url := fmt.Sprintf("http://%s/demo/hello", s.hostIP) + req, err := http.NewRequest("GET", url, nil) + s.NoError(err) + req.Host = "my-other-domain.com" + resp, err := client.Do(req) + + s.NoError(err, s.getProxyConf("")) + if resp != nil { + s.Equal(200, resp.StatusCode, s.getProxyConf("")) + } +} + func (s IntegrationSwarmTestSuite) Test_Config() { s.reconfigureGoDemo("") diff --git a/proxy/ha_proxy_test.go b/proxy/ha_proxy_test.go index 4b0aafbf..4560016d 100644 --- a/proxy/ha_proxy_test.go +++ b/proxy/ha_proxy_test.go @@ -1502,7 +1502,7 @@ func (s HaProxyTestSuite) Test_CreateConfigFromTemplates_ForwardsToHttps_WhenRed acl url_my-service1111_0 path_beg /path acl domain_my-service1111_0 hdr(host) -i my-domain.com acl is_my-service_http hdr(X-Forwarded-Proto) http - redirect scheme https if is_my-service_http url_my-service1111_0 domain_my-service1111_0 + http-request redirect scheme https if is_my-service_http 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, @@ -1527,6 +1527,43 @@ func (s HaProxyTestSuite) Test_CreateConfigFromTemplates_ForwardsToHttps_WhenRed s.Equal(expectedData, actualData) } +func (s HaProxyTestSuite) Test_CreateConfigFromTemplates_ForwardsToDomain_WhenRedirectFromDomainIsSet() { + var actualData string + tmpl := s.TemplateContent + expectedData := fmt.Sprintf( + `%s + acl url_my-service1111_0 path_beg /path + acl domain_my-service1111_0 hdr(host) -i my-domain-1.com my-domain-2.com + http-request redirect code 301 prefix http://my-domain-1.com if { hdr(host) -i my-other-domain-1.com } + http-request redirect code 301 prefix http://my-domain-1.com if { hdr(host) -i my-other-domain-2.com } + 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) + dataInstance.Services["my-service"] = Service{ + ServiceName: "my-service", + PathType: "path_beg", + AclName: "my-service", + ServiceDest: []ServiceDest{ + { + Port: "1111", + ServicePath: []string{"/path"}, + ServiceDomain: []string{"my-domain-1.com", "my-domain-2.com"}, + RedirectFromDomain: []string{"my-other-domain-1.com", "my-other-domain-2.com"}, + }, + }, + } + + p.CreateConfigFromTemplates() + + s.Equal(expectedData, actualData) +} + func (s HaProxyTestSuite) Test_CreateConfigFromTemplates_UsesServiceHeader() { var actualData string tmpl := s.TemplateContent diff --git a/proxy/template.go b/proxy/template.go index feea0659..d441dafe 100644 --- a/proxy/template.go +++ b/proxy/template.go @@ -34,12 +34,17 @@ func getFrontTemplate(s Service) string { acl http_{{.ServiceName}} src_port 80 acl https_{{.ServiceName}} src_port 443 {{- end}} +{{- range $sd := .ServiceDest}} + {{- range $rd := $sd.RedirectFromDomain}} + http-request redirect code 301 prefix http://{{index $sd.ServiceDomain 0}} if { hdr(host) -i {{$rd}} } + {{- end}} +{{- end}} {{- if $.RedirectWhenHttpProto}} {{- range .ServiceDest}} {{- if eq .ReqMode "http"}} {{- if ne .Port ""}} acl is_{{$.AclName}}_http hdr(X-Forwarded-Proto) http - redirect scheme https if is_{{$.AclName}}_http url_{{$.AclName}}{{.Port}}_{{.Index}}{{if .ServiceDomain}} domain_{{$.AclName}}{{.Port}}_{{.Index}}{{end}}{{.SrcPortAclName}} + http-request redirect scheme https if is_{{$.AclName}}_http url_{{$.AclName}}{{.Port}}_{{.Index}}{{if .ServiceDomain}} domain_{{$.AclName}}{{.Port}}_{{.Index}}{{end}}{{.SrcPortAclName}} {{- end}} {{- end}} {{- end}} @@ -147,7 +152,7 @@ backend {{$.ServiceName}}-be{{.Port}}_{{.Index}} http-request deny if !{ ssl_fc } {{- end}} {{- if .HttpsOnly}} - redirect scheme https if !{ ssl_fc } + http-request redirect scheme https if !{ ssl_fc } {{- end}} {{- if eq $.SessionType "sticky-server"}} balance roundrobin diff --git a/proxy/types.go b/proxy/types.go index 86c5d91c..0807e501 100644 --- a/proxy/types.go +++ b/proxy/types.go @@ -2,9 +2,9 @@ package proxy import ( "fmt" + "os" "strconv" "strings" - "os" ) var usersBasePath string = "/run/secrets/dfp_users_%s" @@ -24,6 +24,8 @@ type ServiceDest struct { // The internal port of a service that should be reconfigured. // The port is used only in the *swarm* mode. Port string + // 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`. + RedirectFromDomain []string // The request mode. The proxy should be able to work with any mode supported by HAProxy. // However, actively supported and tested modes are *http*, *tcp*, and *sni*. ReqMode string @@ -388,6 +390,7 @@ func getServiceDest(sr *Service, provider ServiceParameterProvider, index int) S HttpsOnly: getBoolParam(provider, fmt.Sprintf("httpsOnly%s", suffix)), IgnoreAuthorization: getBoolParam(provider, fmt.Sprintf("ignoreAuthorization%s", suffix)), Port: provider.GetString(fmt.Sprintf("port%s", suffix)), + RedirectFromDomain: getSliceFromString(provider, fmt.Sprintf("redirectFromDomain%s", suffix)), ReqMode: reqMode, ServiceDomain: getSliceFromString(provider, fmt.Sprintf("serviceDomain%s", suffix)), ServiceHeader: header, diff --git a/proxy/types_test.go b/proxy/types_test.go index e0bbb447..e0a97c56 100644 --- a/proxy/types_test.go +++ b/proxy/types_test.go @@ -2,10 +2,10 @@ package proxy import ( "github.com/stretchr/testify/suite" + "os" "strconv" "strings" "testing" - "os" ) type TypesTestSuite struct { @@ -229,12 +229,13 @@ func (s *TypesTestSuite) Test_GetServiceFromProvider_MovesServiceDomainToIndexed ServiceDest: []ServiceDest{{ AllowedMethods: []string{}, DeniedMethods: []string{}, + Index: 1, + Port: "1234", + RedirectFromDomain: []string{}, + ReqMode: "reqMode", ServiceDomain: []string{"domain1", "domain2"}, ServiceHeader: map[string]string{}, ServicePath: []string{"/"}, - Port: "1234", - ReqMode: "reqMode", - Index: 1, }}, ServiceName: "serviceName", } @@ -256,12 +257,13 @@ func (s *TypesTestSuite) Test_GetServiceFromProvider_MovesHttpsOnlyToIndexedEntr AllowedMethods: []string{}, DeniedMethods: []string{}, HttpsOnly: true, + Index: 1, + Port: "1234", + RedirectFromDomain: []string{}, + ReqMode: "reqMode", ServiceDomain: []string{}, ServiceHeader: map[string]string{}, ServicePath: []string{"/"}, - Port: "1234", - ReqMode: "reqMode", - Index: 1, }}, ServiceName: "serviceName", } @@ -322,6 +324,7 @@ func (s *TypesTestSuite) getServiceMap(expected Service, indexSuffix, separator "httpsOnly" + indexSuffix: strconv.FormatBool(expected.ServiceDest[0].HttpsOnly), "ignoreAuthorization" + indexSuffix: strconv.FormatBool(expected.ServiceDest[0].IgnoreAuthorization), "port" + indexSuffix: expected.ServiceDest[0].Port, + "redirectFromDomain" + indexSuffix: strings.Join(expected.ServiceDest[0].RedirectFromDomain, separator), "reqMode" + indexSuffix: expected.ServiceDest[0].ReqMode, "serviceDomain" + indexSuffix: strings.Join(expected.ServiceDest[0].ServiceDomain, separator), "serviceHeader" + indexSuffix: header, @@ -355,10 +358,11 @@ func (s *TypesTestSuite) getExpectedService() Service { DenyHttp: true, HttpsOnly: true, IgnoreAuthorization: true, + Port: "1234", + RedirectFromDomain: []string{"sub.domain1", "sub.domain2"}, ServiceDomain: []string{"domain1", "domain2"}, ServiceHeader: map[string]string{"X-Version": "3", "name": "Viktor"}, ServicePath: []string{"/"}, - Port: "1234", ReqMode: "reqMode", UserAgent: UserAgent{Value: []string{"agent-1", "agent-2/replace-with_"}, AclName: "agent_1_agent_2_replace_with_"}, VerifyClientSsl: true, diff --git a/server/server_test.go b/server/server_test.go index c2acfcdb..67d1c753 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -536,6 +536,7 @@ func (s *ServerTestSuite) Test_GetServiceFromUrl_ReturnsProxyService() { DeniedMethods: []string{"PUT", "POST"}, HttpsOnly: true, Port: "1234", + RedirectFromDomain: []string{"sub.domain1", "sub.domain2"}, ReqMode: "reqMode", ServiceDomain: []string{"domain1", "domain2"}, ServiceHeader: map[string]string{"X-Version": "3", "name": "Viktor"}, @@ -555,7 +556,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&serviceCert=%s&outboundHostname=%s&pathType=%s&reqPathSearch=%s&reqPathReplace=%s&templateFePath=%s&templateBePath=%s&timeoutServer=%s&timeoutTunnel=%s&reqMode=%s&httpsOnly=%t&isDefaultBackend=%t&xForwardedProto=%t&redirectWhenHttpProto=%t&httpsPort=%d&serviceDomain=%s&distribute=%t&sslVerifyNone=%t&serviceDomainAlgo=%s&addReqHeader=%s&addResHeader=%s&setReqHeader=%s&setResHeader=%s&delReqHeader=%s&delResHeader=%s&servicePath=/&port=1234&connectionMode=%s&serviceHeader=X-Version:3,name:Viktor&allowedMethods=GET,DELETE&deniedMethods=PUT,POST", + "%s?serviceName=%s&users=%s&usersPassEncrypted=%t&aclName=%s&serviceCert=%s&outboundHostname=%s&pathType=%s&reqPathSearch=%s&reqPathReplace=%s&templateFePath=%s&templateBePath=%s&timeoutServer=%s&timeoutTunnel=%s&reqMode=%s&httpsOnly=%t&isDefaultBackend=%t&xForwardedProto=%t&redirectWhenHttpProto=%t&httpsPort=%d&serviceDomain=%s&redirectFromDomain=%s&distribute=%t&sslVerifyNone=%t&serviceDomainAlgo=%s&addReqHeader=%s&addResHeader=%s&setReqHeader=%s&setResHeader=%s&delReqHeader=%s&delResHeader=%s&servicePath=/&port=1234&connectionMode=%s&serviceHeader=X-Version:3,name:Viktor&allowedMethods=GET,DELETE&deniedMethods=PUT,POST", s.BaseUrl, expected.ServiceName, "user1:pass1,user2:pass2", @@ -577,6 +578,7 @@ func (s *ServerTestSuite) Test_GetServiceFromUrl_ReturnsProxyService() { expected.RedirectWhenHttpProto, expected.HttpsPort, strings.Join(expected.ServiceDest[0].ServiceDomain, ","), + strings.Join(expected.ServiceDest[0].RedirectFromDomain, ","), expected.Distribute, expected.SslVerifyNone, expected.ServiceDomainAlgo, @@ -622,6 +624,7 @@ func (s *ServerTestSuite) Test_GetServiceFromUrl_SetsServicePathToSlash_WhenDoma AllowedMethods: []string{}, DeniedMethods: []string{}, Port: "1234", + RedirectFromDomain: []string{}, ReqMode: "http", ServiceDomain: []string{"domain1", "domain2"}, ServiceHeader: map[string]string{}, diff --git a/stack.yml b/stack.yml index 7ffa8c5c..2f271f2c 100644 --- a/stack.yml +++ b/stack.yml @@ -29,9 +29,9 @@ services: delay: 10s resources: reservations: - memory: 10M - limits: memory: 20M + limits: + memory: 50M docs: image: vfarcic/docker-flow-proxy-docs:${TAG:-latest}