Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Redirect http to https on the same destination #448

Closed
atillamas opened this issue Feb 22, 2018 · 52 comments
Closed

Redirect http to https on the same destination #448

atillamas opened this issue Feb 22, 2018 · 52 comments
Milestone

Comments

@atillamas
Copy link

Hi, is it possible to use fabio to redirect http to https on the same destination? for example redirect http://example.com to https://example.com ? when I've tried it i just get a redirect-loop.

@aaronhurt
Copy link
Member

something like ...

route add http-redirect www.example.com:80 https://www.example.com$path opts "redirect=301"

@aaronhurt
Copy link
Member

when doing http -> https the match host needs to have the port or else you will get a redirect for sure

@atillamas
Copy link
Author

@leprechau Hi. thanks for the suggestion, unfortunately i get a redirect loop using that example

@aaronhurt
Copy link
Member

That's odd ... that's literally the exact copy/paste from our production config with the hostname changed.

@atillamas
Copy link
Author

atillamas commented Feb 22, 2018

@leprechau

In fabio UI:

26 | http-redirect | some.domain.example.com:80/ | https://some.domain.example.com$path | redirect=301 | 100.00% |

And a curl:

curl -iL some.domain.example.com
HTTP/1.1 301 Moved Permanently                    
Date: Thu, 22 Feb 2018 16:39:26 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 65
Connection: keep-alive
Location: https://some.domain.example.com/

HTTP/2 301 
date: Thu, 22 Feb 2018 16:39:26 GMT
content-type: text/html; charset=utf-8
content-length: 65
location: https://some.domain.example.com/

HTTP/2 301 
date: Thu, 22 Feb 2018 16:39:26 GMT
content-type: text/html; charset=utf-8
content-length: 65
location: https://some.domain.example.com/

HTTP/2 301 
date: Thu, 22 Feb 2018 16:39:26 GMT
content-type: text/html; charset=utf-8
content-length: 65
location: https://some.domain.example.com/
<continues>

curl: (47) Maximum (50) redirects followed

If i remove the redirect accessing both http and https://some.domain.example.com works fine.

@aaronhurt
Copy link
Member

So, I'm assuming that go.staging.budbee.com == some.domain.example.com ...

Secondly have you tried removing the trailing slash from the route match?

Meaning some.domain.example.com:80 instead of some.domain.example.com:80/ ... also are you sure the target backend service is not doing any additional redirect? I find it a bit odd that you are getting mixed HTTP/1.1 and HTTP/2 redirects.

@atillamas
Copy link
Author

@leprechau yes that is correct, silly me missed the curl :)
Anyway
In the ui i type in this:
route add http-redirect go.staging.budbee.com:80 https://go.staging.budbee.com$path opts "redirect=301"
but it shows the trailing slash when i look at the rules.
The mixed HTTP/1.1 and HTTP/2 redirecets could be that it's two services listening, one is listening on the original go.staging.budbee.com and one is listening on go.staging.budbee.com/api

@aaronhurt
Copy link
Member

Can you post a full log of ...

curl --http1.1 --max-redirs 3 -vL go.staging.budbee.com

@atillamas
Copy link
Author

curl --http1.1 --max-redirs 3 -vL go.staging.budbee.com
* Rebuilt URL to: go.staging.budbee.com/
*   Trying 54.229.60.69...
* TCP_NODELAY set
* Connected to go.staging.budbee.com (54.229.60.69) port 80 (#0)
> GET / HTTP/1.1
> Host: go.staging.budbee.com
> User-Agent: curl/7.53.1
> Accept: */*
> 
< HTTP/1.1 301 Moved Permanently
< Date: Thu, 22 Feb 2018 17:37:32 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 65
< Connection: keep-alive
< Location: https://go.staging.budbee.com/
< 
* Ignoring the response-body
* Connection #0 to host go.staging.budbee.com left intact
* Issue another request to this URL: 'https://go.staging.budbee.com/'
*   Trying 34.250.238.214...
* TCP_NODELAY set
* Connected to go.staging.budbee.com (34.250.238.214) port 443 (#1)
* Initializing NSS with certpath: sql:/etc/pki/nssdb
*   CAfile: none
  CApath: none
* loaded libnssckbi.so
* ALPN, server accepted to use http/1.1
* SSL connection using TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
* Server certificate:
* 	subject: CN=budbee.com
* 	start date: May 17 00:00:00 2017 GMT
* 	expire date: Jun 17 12:00:00 2018 GMT
* 	common name: budbee.com
* 	issuer: CN=Amazon,OU=Server CA 1B,O=Amazon,C=US
> GET / HTTP/1.1
> Host: go.staging.budbee.com
> User-Agent: curl/7.53.1
> Accept: */*
> 
< HTTP/1.1 301 Moved Permanently
< Date: Thu, 22 Feb 2018 17:37:32 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 65
< Connection: keep-alive
< Location: https://go.staging.budbee.com/
< 
* Ignoring the response-body
* Connection #1 to host go.staging.budbee.com left intact
* Issue another request to this URL: 'https://go.staging.budbee.com/'
* Found bundle for host go.staging.budbee.com: 0x55c7b23436c0 [can pipeline]
* Re-using existing connection! (#1) with host go.staging.budbee.com
* Connected to go.staging.budbee.com (34.250.238.214) port 443 (#1)
> GET / HTTP/1.1
> Host: go.staging.budbee.com
> User-Agent: curl/7.53.1
> Accept: */*
> 
< HTTP/1.1 301 Moved Permanently
< Date: Thu, 22 Feb 2018 17:37:32 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 65
< Connection: keep-alive
< Location: https://go.staging.budbee.com/
< 
* Ignoring the response-body
* Connection #1 to host go.staging.budbee.com left intact
* Issue another request to this URL: 'https://go.staging.budbee.com/'
* Found bundle for host go.staging.budbee.com: 0x55c7b23436c0 [can pipeline]
* Re-using existing connection! (#1) with host go.staging.budbee.com
* Connected to go.staging.budbee.com (34.250.238.214) port 443 (#1)
> GET / HTTP/1.1
> Host: go.staging.budbee.com
> User-Agent: curl/7.53.1
> Accept: */*
> 
< HTTP/1.1 301 Moved Permanently
< Date: Thu, 22 Feb 2018 17:37:32 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 65
< Connection: keep-alive
< Location: https://go.staging.budbee.com/
< 
* Ignoring the response-body
* Connection #1 to host go.staging.budbee.com left intact
* Maximum (3) redirects followed
curl: (47) Maximum (3) redirects followed

@aaronhurt
Copy link
Member

So it looks like you switch to a different host for the port 443 request than the port 80 request. Is it possible there is a difference in configuration between those two hosts?

@atillamas
Copy link
Author

atillamas commented Feb 22, 2018

They are running behind an ALB in different docker-containers, 2 with go.staging.budbee.com and 2 with go.staging.budbee.com/api and the configuration is identical. Only different is the random listening port they get allocated.

@aaronhurt
Copy link
Member

Something really strange is happening ... it's almost like there are two redirect rules defined. The first request clearly matches go.staging.budbee.com:80 that you have defined for the posted http-redirect. However it looks like the second request to go.staging.budbee.com:443 is also matching a redirect route. That could be a specific route for https://go.staging.budbee.com/ or a plain non-port specific host route go.staging.budbee.com.

@magiconair
Copy link
Contributor

@atillamas Can you post your full routing table with curl localhost:9998/api/routes?raw ?

@atillamas
Copy link
Author

@magiconair Sure thing, i created a completely new stack, exact copy of our staging, just running one single job, exactly the same behaviour

job "http-echo" {
    datacenters = ["eu-west-1"]
    group "service" {
        count = 2
	constraint {
		operator = "distinct_hosts"
		value = "true"
	}
        update {
                max_parallel = 1
                min_healthy_time = "30s"
                healthy_deadline = "5m"
        }
        task "http-echo" {
            driver = "docker"
            config {
                image = "hashicorp/http-echo"
                port_map {
                        echo = 5678
                }
                args  = ["-text", " version4: ${node.unique.name}"]
            }
            service {
                name = "http-echo"
                tags = ["http-echo","urlprefix-echo.staging.budbee.com/"]
                port = "echo"
                check {
                    type     = "http"
                    path     = "/"
                    interval = "5s"
                    timeout  = "2s"
                    port     = "echo"
                }
            }
            resources {
                cpu = 500
                network {
                    port "echo" { }
                }
            }
        }
    }

ubuntu@ip-10-0-62-158:~$ curl localhost:9998/api/routes?raw
route add http-redirect echo.staging.budbee.com:80/ https://echo.staging.budbee.com$path opts "redirect=301"
route add http-echo echo.staging.budbee.com/ http://10.0.62.158:26296/ tags "http-echo"
route add http-echo echo.staging.budbee.com/ http://10.0.61.154:23384/ tags "http-echo"
curl --http1.1 --max-redirs 3 -vL echo.staging.budbee.com
* Rebuilt URL to: echo.staging.budbee.com/
*   Trying 52.30.193.125...
* TCP_NODELAY set
* Connected to echo.staging.budbee.com (52.30.193.125) port 80 (#0)
> GET / HTTP/1.1
> Host: echo.staging.budbee.com
> User-Agent: curl/7.55.1
> Accept: */*
> 
< HTTP/1.1 301 Moved Permanently
< Date: Thu, 22 Feb 2018 20:36:58 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 67
< Connection: keep-alive
< Location: https://echo.staging.budbee.com/
< 
* Ignoring the response-body
* Connection #0 to host echo.staging.budbee.com left intact
* Issue another request to this URL: 'https://echo.staging.budbee.com/'
*   Trying 52.30.193.125...
* TCP_NODELAY set
* Connected to echo.staging.budbee.com (52.30.193.125) port 443 (#1)
* ALPN, offering http/1.1
* Cipher selection: PROFILE=SYSTEM
* successfully set certificate verify locations:
*   CAfile: /etc/pki/tls/certs/ca-bundle.crt
  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Client hello (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
* ALPN, server accepted to use http/1.1
* Server certificate:
*  subject: CN=budbee.com
*  start date: May 17 00:00:00 2017 GMT
*  expire date: Jun 17 12:00:00 2018 GMT
*  subjectAltName: host "echo.staging.budbee.com" matched cert's "*.staging.budbee.com"
*  issuer: C=US; O=Amazon; OU=Server CA 1B; CN=Amazon
*  SSL certificate verify ok.
> GET / HTTP/1.1
> Host: echo.staging.budbee.com
> User-Agent: curl/7.55.1
> Accept: */*
> 
< HTTP/1.1 301 Moved Permanently
< Date: Thu, 22 Feb 2018 20:36:58 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 67
< Connection: keep-alive
< Location: https://echo.staging.budbee.com/
< 
* Ignoring the response-body
* Connection #1 to host echo.staging.budbee.com left intact
* Issue another request to this URL: 'https://echo.staging.budbee.com/'
* Found bundle for host echo.staging.budbee.com: 0x55795ae5a090 [can pipeline]
* Re-using existing connection! (#1) with host echo.staging.budbee.com
* Connected to echo.staging.budbee.com (52.30.193.125) port 443 (#1)
> GET / HTTP/1.1
> Host: echo.staging.budbee.com
> User-Agent: curl/7.55.1
> Accept: */*
> 
< HTTP/1.1 301 Moved Permanently
< Date: Thu, 22 Feb 2018 20:36:58 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 67
< Connection: keep-alive
< Location: https://echo.staging.budbee.com/
< 
* Ignoring the response-body
* Connection #1 to host echo.staging.budbee.com left intact
* Issue another request to this URL: 'https://echo.staging.budbee.com/'
* Found bundle for host echo.staging.budbee.com: 0x55795ae5a090 [can pipeline]
* Re-using existing connection! (#1) with host echo.staging.budbee.com
* Connected to echo.staging.budbee.com (52.30.193.125) port 443 (#1)
> GET / HTTP/1.1
> Host: echo.staging.budbee.com
> User-Agent: curl/7.55.1
> Accept: */*
> 
< HTTP/1.1 301 Moved Permanently
< Date: Thu, 22 Feb 2018 20:36:58 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 67
< Connection: keep-alive
< Location: https://echo.staging.budbee.com/
< 
* Ignoring the response-body
* Connection #1 to host echo.staging.budbee.com left intact
* Maximum (3) redirects followed
curl: (47) Maximum (3) redirects followed

@aaronhurt
Copy link
Member

Hrm.... I wonder ... what if you try something like ...

route add http-redirect echo.staging.budbee.com:80/ https://echo.staging.budbee.com$path opts "redirect=301"
route add http-echo / http://10.0.62.158:26296/ tags "http-echo"
route add http-echo / http://10.0.61.154:23384/ tags "http-echo"

@aaronhurt
Copy link
Member

So in the service definition ...

                name = "http-echo"
                tags = ["http-echo","urlprefix-/"]

@atillamas
Copy link
Author

ubuntu@ip-10-0-62-158:~$ curl localhost:9998/api/routes?raw
route add http-redirect echo.staging.budbee.com:80/ https://echo.staging.budbee.com$path opts "redirect=301"
route add http-echo / http://10.0.62.158:26296/ tags "http-echo"
route add http-echo / http://10.0.61.154:23384/ tags "http-echo"
tags = ["http-echo","urlprefix-/"]
 curl --http1.1 --max-redirs 3 -vL echo.staging.budbee.com
* Rebuilt URL to: echo.staging.budbee.com/
*   Trying 52.19.94.85...
* TCP_NODELAY set
* Connected to echo.staging.budbee.com (52.19.94.85) port 80 (#0)
> GET / HTTP/1.1
> Host: echo.staging.budbee.com
> User-Agent: curl/7.55.1
> Accept: */*
> 
< HTTP/1.1 301 Moved Permanently
< Date: Thu, 22 Feb 2018 20:54:37 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 67
< Connection: keep-alive
< Location: https://echo.staging.budbee.com/
< 
* Ignoring the response-body
* Connection #0 to host echo.staging.budbee.com left intact
* Issue another request to this URL: 'https://echo.staging.budbee.com/'
*   Trying 52.19.94.85...
* TCP_NODELAY set
* Connected to echo.staging.budbee.com (52.19.94.85) port 443 (#1)
* ALPN, offering http/1.1
* Cipher selection: PROFILE=SYSTEM
* successfully set certificate verify locations:
*   CAfile: /etc/pki/tls/certs/ca-bundle.crt
  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Client hello (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
* ALPN, server accepted to use http/1.1
* Server certificate:
*  subject: CN=budbee.com
*  start date: May 17 00:00:00 2017 GMT
*  expire date: Jun 17 12:00:00 2018 GMT
*  subjectAltName: host "echo.staging.budbee.com" matched cert's "*.staging.budbee.com"
*  issuer: C=US; O=Amazon; OU=Server CA 1B; CN=Amazon
*  SSL certificate verify ok.
> GET / HTTP/1.1
> Host: echo.staging.budbee.com
> User-Agent: curl/7.55.1
> Accept: */*
> 
< HTTP/1.1 301 Moved Permanently
< Date: Thu, 22 Feb 2018 20:54:37 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 67
< Connection: keep-alive
< Location: https://echo.staging.budbee.com/
< 
* Ignoring the response-body
* Connection #1 to host echo.staging.budbee.com left intact
* Issue another request to this URL: 'https://echo.staging.budbee.com/'
* Found bundle for host echo.staging.budbee.com: 0x55c9cb3c4090 [can pipeline]
* Re-using existing connection! (#1) with host echo.staging.budbee.com
* Connected to echo.staging.budbee.com (52.19.94.85) port 443 (#1)
> GET / HTTP/1.1
> Host: echo.staging.budbee.com
> User-Agent: curl/7.55.1
> Accept: */*
> 
< HTTP/1.1 301 Moved Permanently
< Date: Thu, 22 Feb 2018 20:54:37 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 67
< Connection: keep-alive
< Location: https://echo.staging.budbee.com/
< 
* Ignoring the response-body
* Connection #1 to host echo.staging.budbee.com left intact
* Issue another request to this URL: 'https://echo.staging.budbee.com/'
* Found bundle for host echo.staging.budbee.com: 0x55c9cb3c4090 [can pipeline]
* Re-using existing connection! (#1) with host echo.staging.budbee.com
* Connected to echo.staging.budbee.com (52.19.94.85) port 443 (#1)
> GET / HTTP/1.1
> Host: echo.staging.budbee.com
> User-Agent: curl/7.55.1
> Accept: */*
> 
< HTTP/1.1 301 Moved Permanently
< Date: Thu, 22 Feb 2018 20:54:38 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 67
< Connection: keep-alive
< Location: https://echo.staging.budbee.com/
< 
* Ignoring the response-body
* Connection #1 to host echo.staging.budbee.com left intact
* Maximum (3) redirects followed
curl: (47) Maximum (3) redirects followed

@aaronhurt
Copy link
Member

Okay, I'm stumped. The above config looks pretty simple to reproduce I'll see if I can make this happen locally.

@aaronhurt
Copy link
Member

aaronhurt commented Feb 22, 2018

One last thought ... you have the AWS load balancers in-front of fabio in this case, right? I don't have much experience with AWS ... is it possible this is a case where the two load balancers are fighting? Is the AWS load balancer terminating SSL and then forwarding as plain HTTP back to fabio? Do you have a way to test direct to the fabio instance without going through the AWS load balancer?

@atillamas
Copy link
Author

Yes the ALB (Application Load Balancer) is in-front of Fabio and terminating the SSL. I'm starting to believe that this is the issue also, i bet a normal Network Loadbalancer would work, i choosed the ALB to quickly get a way to be able to control access to our "internal" public exposed services with WAF, I now see that Fabio has support for IP-restriction so i might try to migrate it over to a Network Load Balancer instead.

Haven't been able to test it without going through the loadbalancer in an easy way. If I run a curl with host header behind the loadbalancer the first request goes directly to Fabio, but then the next request go through the LB and into the redirect loop...

@aaronhurt
Copy link
Member

Looks related to #450 ... is this the same issue?

@atillamas
Copy link
Author

@leprechau it appears so.

tcpdump of the headers

HEAD / HTTP/1.1
X-Forwarded-For: 178.132.77.232
X-Forwarded-Proto: http
X-Forwarded-Port: 80
Host: echo.staging.budbee.com
X-Amzn-Trace-Id: Root=1-5a951fae-4604c7d214c51fcc8dea3620
User-Agent: curl/7.55.1
Accept: */*

HTTP/1.1 301 Moved Permanently
Content-Type: text/html; charset=utf-8
Location: https://echo.staging.budbee.com/
Date: Tue, 27 Feb 2018 09:06:54 GMT
HEAD / HTTP/1.1
X-Forwarded-For: 178.132.77.232
X-Forwarded-Proto: https
X-Forwarded-Port: 443
Host: echo.staging.budbee.com
X-Amzn-Trace-Id: Root=1-5a951fae-a95257c2eb57be56fd699d02
user-agent: curl/7.55.1
accept: */*

HTTP/1.1 301 Moved Permanently
Content-Type: text/html; charset=utf-8
Location: https://echo.staging.budbee.com/
Date: Tue, 27 Feb 2018 09:06:54 GMT
HEAD / HTTP/1.1
X-Forwarded-For: 178.132.77.232
X-Forwarded-Proto: https
X-Forwarded-Port: 443
Host: echo.staging.budbee.com
X-Amzn-Trace-Id: Root=1-5a951fae-3295da6666d938b6b009b2ae
user-agent: curl/7.55.1
accept: */*

@atillamas
Copy link
Author

This code fixes the redirect problem for me but introduces the problem that i don't get any content at all :)
I wanted to put the if-statement together on this line: https://github.com/fabiolb/fabio/blob/master/proxy/http_proxy.go#L101
but just couldn't get it to work with:
if t.RedirectCode != 0 && (strings.HasPrefix(t.GetRedirectURL(requestURL).String(), "https://") && r.Header.Get("X-Forwarded-Port") != "443") {
Something with the logic is off, but i don't understand what. Maybe you can take a quick look?

git diff proxy/http_proxy.go 
diff --git a/proxy/http_proxy.go b/proxy/http_proxy.go
index 0898d7a..3778409 100644
--- a/proxy/http_proxy.go
+++ b/proxy/http_proxy.go
@@ -100,7 +100,9 @@ func (p *HTTPProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 
        if t.RedirectCode != 0 {
                redirectURL := t.GetRedirectURL(requestURL)
-               http.Redirect(w, r, redirectURL.String(), t.RedirectCode)
+               if strings.HasPrefix(redirectURL.String(), "https://") && r.Header.Get("X-Forwarded-Port") != "443" {
+                       http.Redirect(w, r, redirectURL.String(), t.RedirectCode)
+               }

@aaronhurt
Copy link
Member

@atillamas Yes, I think this is the same problem being described in #450 for sure and you're already on the same track.

I was just looking at that same spot in the code but I think the logic needs to go elsewhere. Basically we need it to not even mark the target as a redirect which would happen much earlier.

@aaronhurt
Copy link
Member

@atillamas maybe something like this in the Lookup function in table.go around line 318 ...

	// find matching hosts for the request
	// and add "no host" as the fallback option
	hosts := t.matchingHosts(req)
	hosts = append(hosts, "")
	for _, h := range hosts {
		if target = t.lookup(h, path, trace, pick, match); target != nil {
			if target.RedirectCode != 0 {
				redirectURL := target.GetRedirectURL(requestURL)
				if redirectURL.Scheme == "https" && req.Header.Get("X-Forwarded-Port") != "443" {
					continue
				}
			}
			break
		}
	}

Completely untested but I think we need to prevent the redirect route from being returned by the lookup function based on your condition.

@aaronhurt
Copy link
Member

Maybe try something like that and post some logs with tracing enabled?

@atillamas
Copy link
Author

@leprechau Thanks for the suggestion, Unfortunately I get the redirect loop when implementing the code there, I'll see if i can find some time during the weekend to troubleshoot some more.

@aaronhurt
Copy link
Member

aaronhurt commented Mar 9, 2018

@atillamas gotcha ... sorry about that. I was just trying to think of how to prevent the redirect from occurring and get the "correct" target returned. Maybe it could be as simple as still returning the target but setting the redirect code to 0 ... that would effectively return the same route target but ignore the redirect option.

@atillamas
Copy link
Author

@leprechau No worries at all, I'm grateful for all your help!
Unfortunately I haven't had time doing anything more with this, but I will when I get time, although it would probably be a lot faster if someone with knowledge dug in instead :)

@aaronhurt
Copy link
Member

@atillamas understood. I'll try and take a stab at a few options using your example route table above.

@aaronhurt
Copy link
Member

Okay, so I think I have working code but I'm not sure it's the best way as it assumes that an upstream https source is always going to be 443 but maybe that's okay.

The patch ...

diff --git a/route/table.go b/route/table.go
index 8e74bde..898ff1a 100644
--- a/route/table.go
+++ b/route/table.go
@@ -326,6 +326,10 @@ func (t Table) Lookup(req *http.Request, trace string, pick picker, match matche
        hosts = append(hosts, "")
        for _, h := range hosts {
                if target = t.lookup(h, path, trace, pick, match); target != nil {
+                       if target.RedirectCode != 0 && target.URL.Scheme == "https" && req.Header.Get("X-Forwarded-Port") == "443" {
+                               log.Print("[DEBUG] Ignoring https redirect from https upstream")
+                               continue
+                       }
                        break
                }
        }

The test table:

route add http-redirect echo.localtest.me:80/ https://echo.localtest.me/$path opts "redirect=301"
route add http-echo1 echo.localtest.me/ http://localhost:15678/ tags "http-echo1"
route add http-echo2 echo.localtest.me/ http://localhost:25678/ tags "http-echo2"

Running fabio as:

sudo ./fabio -log.level=TRACE -proxy.addr=":80" -insecure

XFP 80 request:

$ curl -vvv -H "Trace: xxx" -H "X-Forwarded-Port: 80" http://echo.localtest.me/
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to echo.localtest.me (127.0.0.1) port 80 (#0)
> GET / HTTP/1.1
> Host: echo.localtest.me
> User-Agent: curl/7.54.0
> Accept: */*
> Trace: xxx
> X-Forwarded-Port: 80
>
< HTTP/1.1 301 Moved Permanently
< Content-Type: text/html; charset=utf-8
< Location: https://echo.localtest.me/
< Date: Tue, 13 Mar 2018 20:20:13 GMT
< Content-Length: 61
<
<a href="https://echo.localtest.me/">Moved Permanently</a>.

* Connection #0 to host echo.localtest.me left intact
2018/03/13 15:20:13 [TRACE] xxx Tracing echo.localtest.me/
2018/03/13 15:20:13 [TRACE] xxx Match echo.localtest.me:80/
2018/03/13 15:20:13 [TRACE] xxx Routing to service http-redirect on https://echo.localtest.me/$path

XFP 443 request:

$ curl -vvv -H "Trace: xxx" -H "X-Forwarded-Port: 443" http://echo.localtest.me/
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to echo.localtest.me (127.0.0.1) port 80 (#0)
> GET / HTTP/1.1
> Host: echo.localtest.me
> User-Agent: curl/7.54.0
> Accept: */*
> Trace: xxx
> X-Forwarded-Port: 443
>
< HTTP/1.1 200 OK
< Content-Length: 6
< Content-Type: text/plain; charset=utf-8
< Date: Tue, 13 Mar 2018 20:20:39 GMT
< X-App-Name:
< X-App-Version: 0.2.4
<
hello
* Connection #0 to host echo.localtest.me left intact
2018/03/13 15:20:39 [TRACE] xxx Tracing echo.localtest.me/
2018/03/13 15:20:39 [TRACE] xxx Match echo.localtest.me:80/
2018/03/13 15:20:39 [DEBUG] Ignoring https redirect from https upstream
2018/03/13 15:20:39 [TRACE] xxx Match echo.localtest.me/
2018/03/13 15:20:39 [TRACE] xxx Routing to service http-echo2 on http://localhost:25678/

@aaronhurt
Copy link
Member

From the caps you posted above it looks like AWS is also sending X-Forwarded-Proto ... that might be better to check than X-Forwarded-Port.

@aaronhurt
Copy link
Member

https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/x-forwarded-headers.html#x-forwarded-proto

Just confirmed this diff also works ... new tests and config below ...

The patch:

diff --git a/route/table.go b/route/table.go
index 8e74bde..0ac8dd2 100644
--- a/route/table.go
+++ b/route/table.go
@@ -326,6 +326,10 @@ func (t Table) Lookup(req *http.Request, trace string, pick picker, match matche
        hosts = append(hosts, "")
        for _, h := range hosts {
                if target = t.lookup(h, path, trace, pick, match); target != nil {
+                       if target.RedirectCode != 0 && target.URL.Scheme == "https" && req.Header.Get("X-Forwarded-Proto") == "https" {
+                               log.Print("[DEBUG] Ignoring https redirect from https upstream")
+                               continue
+                       }
                        break
                }
        }

The test table:

route add http-redirect echo.localtest.me:80/ https://echo.localtest.me/$path opts "redirect=301"
route add http-echo1 / http://localhost:15678/ tags "http-echo1"
route add http-echo2 / http://localhost:25678/ tags "http-echo2"

Running fabio as:

sudo ./fabio -log.level=TRACE -proxy.addr=":80" -insecure

X-Forwarded-Proto http request:

$ curl -vvv -H "Trace: xxx" -H "X-Forwarded-Proto: http" http://echo.localtest.me/
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to echo.localtest.me (127.0.0.1) port 80 (#0)
> GET / HTTP/1.1
> Host: echo.localtest.me
> User-Agent: curl/7.54.0
> Accept: */*
> Trace: xxx
> X-Forwarded-Proto: http
>
< HTTP/1.1 301 Moved Permanently
< Content-Type: text/html; charset=utf-8
< Location: https://echo.localtest.me/
< Date: Tue, 13 Mar 2018 20:36:43 GMT
< Content-Length: 61
<
<a href="https://echo.localtest.me/">Moved Permanently</a>.

* Connection #0 to host echo.localtest.me left intact
2018/03/13 15:36:43 [TRACE] xxx Tracing echo.localtest.me/
2018/03/13 15:36:43 [TRACE] xxx Match echo.localtest.me:80/
2018/03/13 15:36:43 [TRACE] xxx Routing to service http-redirect on https://echo.localtest.me/$path

X-Forwarded-Proto https:

$ curl -vvv -H "Trace: xxx" -H "X-Forwarded-Proto: https" http://echo.localtest.me/
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to echo.localtest.me (127.0.0.1) port 80 (#0)
> GET / HTTP/1.1
> Host: echo.localtest.me
> User-Agent: curl/7.54.0
> Accept: */*
> Trace: xxx
> X-Forwarded-Proto: https
>
< HTTP/1.1 200 OK
< Content-Length: 6
< Content-Type: text/plain; charset=utf-8
< Date: Tue, 13 Mar 2018 20:35:10 GMT
< X-App-Name:
< X-App-Version: 0.2.4
<
hello
* Connection #0 to host echo.localtest.me left intact
2018/03/13 15:35:10 [TRACE] xxx Tracing echo.localtest.me/
2018/03/13 15:35:10 [TRACE] xxx Match echo.localtest.me:80/
2018/03/13 15:35:10 [DEBUG] Ignoring https redirect from https upstream
2018/03/13 15:35:10 [TRACE] xxx Match /
2018/03/13 15:35:10 [TRACE] xxx Routing to service http-echo1 on http://localhost:15678/

@aaronhurt
Copy link
Member

aaronhurt commented Mar 13, 2018

Maybe the log message should be TRACE instead of DEBUG .. but it works. Just not sure if there are side affects unknown. The builtin test suite still passes with the above patch applied.

@aaronhurt
Copy link
Member

I'll open a PR for further discussion.

aaronhurt pushed a commit to myENA/fabio that referenced this issue Mar 13, 2018
The AWS load balancer terminates SSL and passes plain HTTP
to fabio but does not offer the ability to do a schema redirect.

Therefore, fabio running behind an AWS ELB with a schema
redirect rule results in a redirect loop.  This PR fixes fabiolb#448.
@magiconair
Copy link
Contributor

I'll comment here to keep the discussion in one place.

Why only https?

Maybe like this?

target.RedirectCode != 0 && target.URL.Scheme == req.Header.Get("X-Forwarded-Proto")

@aaronhurt
Copy link
Member

I thought about that ... but would you want to skip an upstream http request that matched an http redirect to a different location? I guess what we really want is to only do the redirect if the upstream scheme == redirect target scheme AND upstream host == redirect target host. I'm not sure how we get the upstream host though.

@magiconair
Copy link
Contributor

But will this change not have the same effect for https targets, e.g. https://foo.com/bar -> https://foo.com/baz?

@aaronhurt
Copy link
Member

aaronhurt commented Mar 14, 2018

That's a very good point.

So more generically we want to prevent redirect loops. The initial case was a simple scheme redirect (http -> https) but that does not cover all use cases.

How about this ...

  1. target.RedirectCode != 0
  2. target.URL.Scheme == X-Forwarded-Proto
  3. target.URL.Hostname() == req.URL.Hostname()
  4. target.URL.Path == req.URL.Path

If all the above are true ... skip to prevent a redirect loop.

@aaronhurt
Copy link
Member

I added another commit to the PR to address these cases and make the redirect protection more generic while still satisfying the original request.

@atillamas
Copy link
Author

atillamas commented Mar 15, 2018

@leprechau Thanks for this. I got it to work with your patch if i removed this check:

target.URL.Hostname() == req.URL.Hostname() && target.URL.Path == req.URL.Path

this is the content of all the variables when doing a request (values inside ' '):

target.RedirectCode '301', target.URL.Scheme 'https', req.Header.Get("X-Forwarded-Proto"): 'https', target.URL.Hostname(): 'echo.staging.budbee.com', req.URL.Hostname(): '', target.URL.Path: '', req.URL.Path: '/'

and this is the config used:

tags = ["http-echo","urlprefix-echo.staging.budbee.com:80/  redirect=301,https://echo.staging.budbee.com","urlprefix-/"]
curl localhost:9998/api/routes?raw
route add http-echo echo.staging.budbee.com:80/ https://echo.staging.budbee.com tags "http-echo" opts "redirect=301"
route add http-echo / http://10.0.62.211:29790/ tags "http-echo"

@aaronhurt
Copy link
Member

aaronhurt commented Mar 15, 2018

Interesting ... wonder how the request hostname is empty? The path I can see that / and empty should be considered the same. Maybe a blank host should be considered a match as well?

@atillamas
Copy link
Author

atillamas commented Mar 15, 2018

@leprechau this is the content of the entire req object:

'&{GET / HTTP/1.1 1 1 map[User-Agent:[curl/7.55.1] Accept:[*/*] X-Forwarded-For:[178.132.77.196] X-Forwarded-Proto:[https] X-Forwarded-Port:[443] X-Amzn-Trace-Id:[Root=1-5aaafeb6-9b7eaa7054e311c6f394c52c]] {} <nil> 0 [] false echo.staging.budbee.com map[] map[] <nil> map[] 10.0.62.195:19938 / <nil> <nil> <nil> 0xc420333900}'

Looks like it should be:
req.Host

and it works with:

if target.RedirectCode != 0 && target.URL.Scheme == req.Header.Get("X-Forwarded-Proto") && target.URL.Hostname() == req.Host {

@aaronhurt
Copy link
Member

aaronhurt commented Mar 16, 2018

Yes, I reworked it an pushed another commit to PR #466 ... can you see if this now works in your AWS environment as expected? I was trying to avoid calling GetRedirectURL in the lookup function initially. However, that's probably where it belongs. I renamed GetRedirectURL to BuildRedirectURL and modified it's behavior to cache the build URL object in the target. This means we can call it in the lookup function (when target.RedirectCode !=0) and not have to call it again in http_proxy.go.

@atillamas
Copy link
Author

@leprechau Nice work. Almost there now :)
Just one tiny problem: the matching of the target.RedirectURL.Path == req.URL.Path does not work. Without it everything seems to be working fine.
Looks like target.RedirectURL.Path is empty

@aaronhurt
Copy link
Member

I’m assuming in this case that req.URL.Path is just / and that should be the default for the redirect target? I’ll make another update shortly.

@atillamas
Copy link
Author

@leprechau Yes if i add the trailing / in the redirect redirect=301,https://echo.staging.budbee.com/ everything works!

@aaronhurt
Copy link
Member

aaronhurt commented Mar 16, 2018

@atillamas Great! Just pushed one last commit that should make it work without having to fully qualify the redirect target in your config.

@atillamas
Copy link
Author

@leprechau forgot to say that it worked with $path previously also, But now I've tested your latest change and it does work as it should! 👍 great job! Thanks!
Everything seems to work exactly as it should!

@aaronhurt
Copy link
Member

Awesome, thanks for the patience and testing.

@magiconair magiconair added this to the 1.5.9 milestone Mar 19, 2018
@magiconair
Copy link
Contributor

I've merged the PR. Thanks a lot for the incredible patience and diligence of both of you!

@aaronhurt
Copy link
Member

@magiconair Thank you! Without you Fabio wouldn't exist :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants