Repository: trinodb/trino (Java)
Date: 2020-02-25 23:54:18+00:00
Task type: Bug Fix

Issue:
New WebUI Authentication Manager Breaks Forwarded HTTPS
We use a load balancer in front of Presto to handle HTTPS.  The load balancer forwards to Presto on port 8080.  We enable `http-server.authentication.allow-forwarded-https` to support authentication in this scenario.

Running a snapshot of master, I am not able to reach Presto through the load balancer.  Browsing to `https://presto.domain.com` delivers a 303 status code, redirecting to `https://presto.domain.com:80/ui`.  This is incorrect, as no one is listening on port 80.


Issue discussion:
Configuring the load balancer to listen on a port besides 443 works around the issue.  `https://presto.domain.com:4433` is accessible.
> Browsing to `https://presto.domain.com` delivers a 303 status code, redirecting to `https://presto.domain.com:80/ui`.

Is your load balancer sending `X-Forwarded-For`, `X-Forwarded-Proto` headers?
It’s an AWS Application Load Balancer. It’s certainly sending
X-Forwarded-Proto as Presto recognizes that traffic is encrypted. If it’s
not sending X-Forwarded-For then whatever header it is supplying should be
supported as well. But I assume it is sending the default headers.

On Sat, Feb 22, 2020 at 6:03 AM Piotr Findeisen <notifications@github.com>
wrote:

> Browsing to https://presto.domain.com delivers a 303 status code,
> redirecting to https://presto.domain.com:80/ui.
>
> Is your load balancer sending X-Forwarded-For, X-Forwarded-Proto headers?
>
> —
> You are receiving this because you authored the thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/prestosql/presto/issues/2908?email_source=notifications&email_token=AAJ4W3XB7BLQJH53HEFNER3REEPAZA5CNFSM4KZMHD32YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEMU7ZLQ#issuecomment-589954222>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AAJ4W3VBOX2NKRBAIBPGXQDREEPAZANCNFSM4KZMHD3Q>
> .
>

@findepi this doesn't seem to have fixed the issue for me?

As I mentioned in slack, I don't usually build Presto, but I'm really quite sure I'm running `head`, and am still getting redirected incorrectly.

I captured the headers the ALB is sending:
```
GET / HTTP/1.1
X-Forwarded-For: <MY IP>
X-Forwarded-Proto: https
X-Forwarded-Port: 443
Host: presto.domain.net
X-Amzn-Trace-Id: Root=1-5e546e4c-4d18dc28137ff5ce1b626b72
cache-control: max-age=0
upgrade-insecure-requests: 1
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36
sec-fetch-user: ?1
accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
sec-fetch-site: none
sec-fetch-mode: navigate
accept-encoding: gzip, deflate, br
accept-language: en-US,en;q=0.9
```

Delivering this to Presto results in the following response:
```
content-length: 0
date: Tue, 25 Feb 2020 00:49:03 GMT
location: http://presto.domain.net:80/ui/
status: 303
```
The code is activating only when `X-Forwarded-Host` is present, which isn't required.

Solution patch:
diff --git a/presto-main/src/main/java/io/prestosql/server/ui/FormWebUiAuthenticationManager.java b/presto-main/src/main/java/io/prestosql/server/ui/FormWebUiAuthenticationManager.java
index 4c0a7faa79ad..cb77ebfc1a80 100644
--- a/presto-main/src/main/java/io/prestosql/server/ui/FormWebUiAuthenticationManager.java
+++ b/presto-main/src/main/java/io/prestosql/server/ui/FormWebUiAuthenticationManager.java
@@ -15,6 +15,8 @@
 
 import com.google.common.hash.Hashing;
 import com.google.common.io.ByteStreams;
+import com.google.common.net.HostAndPort;
+import com.google.common.primitives.Ints;
 import io.airlift.http.client.HttpUriBuilder;
 import io.jsonwebtoken.JwtException;
 import io.jsonwebtoken.Jwts;
@@ -47,6 +49,7 @@
 import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Strings.emptyToNull;
 import static com.google.common.base.Strings.isNullOrEmpty;
+import static com.google.common.net.HttpHeaders.HOST;
 import static com.google.common.net.HttpHeaders.LOCATION;
 import static com.google.common.net.HttpHeaders.WWW_AUTHENTICATE;
 import static com.google.common.net.HttpHeaders.X_FORWARDED_HOST;
@@ -54,7 +57,6 @@
 import static com.google.common.net.HttpHeaders.X_FORWARDED_PROTO;
 import static io.airlift.http.client.HttpUriBuilder.uriBuilder;
 import static io.prestosql.server.HttpRequestSessionContext.AUTHENTICATED_IDENTITY;
-import static java.lang.Integer.parseInt;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Arrays.stream;
 import static java.util.Objects.requireNonNull;
@@ -318,22 +320,7 @@ private static String getRedirectLocation(HttpServletRequest request, String pat
 
     static String getRedirectLocation(HttpServletRequest request, String path, String queryParameter)
     {
-        HttpUriBuilder builder;
-        if (isNullOrEmpty(request.getHeader(X_FORWARDED_HOST))) {
-            // not forwarded
-            builder = uriBuilder()
-                    .scheme(request.getScheme())
-                    .host(request.getServerName())
-                    .port(request.getServerPort());
-        }
-        else {
-            // forwarded
-            builder = uriBuilder()
-                    .scheme(firstNonNull(emptyToNull(request.getHeader(X_FORWARDED_PROTO)), request.getScheme()))
-                    .host(request.getHeader(X_FORWARDED_HOST));
-            getForwarderPort(request).ifPresent(builder::port);
-        }
-
+        HttpUriBuilder builder = toUriBuilderWithForwarding(request);
         builder.replacePath(path);
         if (queryParameter != null) {
             builder.addParameter(queryParameter);
@@ -371,15 +358,25 @@ private static String parseJwt(byte[] hmac, String jwt)
                 .getSubject();
     }
 
-    private static Optional<Integer> getForwarderPort(HttpServletRequest request)
+    private static HttpUriBuilder toUriBuilderWithForwarding(HttpServletRequest request)
     {
-        if (!isNullOrEmpty(request.getHeader(X_FORWARDED_PORT))) {
-            try {
-                return Optional.of(parseInt(request.getHeader(X_FORWARDED_PORT)));
-            }
-            catch (ArithmeticException ignore) {
-            }
+        HttpUriBuilder builder;
+        if (isNullOrEmpty(request.getHeader(X_FORWARDED_PROTO)) && isNullOrEmpty(request.getHeader(X_FORWARDED_HOST))) {
+            // not forwarded
+            builder = uriBuilder()
+                    .scheme(request.getScheme())
+                    .hostAndPort(HostAndPort.fromString(request.getHeader(HOST)));
         }
-        return Optional.empty();
+        else {
+            // forwarded
+            builder = uriBuilder()
+                    .scheme(firstNonNull(emptyToNull(request.getHeader(X_FORWARDED_PROTO)), request.getScheme()))
+                    .hostAndPort(HostAndPort.fromString(firstNonNull(emptyToNull(request.getHeader(X_FORWARDED_HOST)), request.getHeader(HOST))));
+
+            Optional.ofNullable(emptyToNull(request.getHeader(X_FORWARDED_PORT)))
+                    .map(Ints::tryParse)
+                    .ifPresent(builder::port);
+        }
+        return builder;
     }
 }


Test patch:
diff --git a/presto-main/src/test/java/io/prestosql/server/ui/TestWebUi.java b/presto-main/src/test/java/io/prestosql/server/ui/TestWebUi.java
index 192dd4827cb0..279e5a6363f8 100644
--- a/presto-main/src/test/java/io/prestosql/server/ui/TestWebUi.java
+++ b/presto-main/src/test/java/io/prestosql/server/ui/TestWebUi.java
@@ -127,7 +127,7 @@ private void testLoggedOut(URI baseUri)
     {
         assertRedirect(client, getUiLocation(baseUri), getLoginHtmlLocation(baseUri));
 
-        assertRedirect(client, getLocation(baseUri, "/ui/query.html", "abc123"), getLocation(baseUri, "/ui/login.html", "/ui/query.html?abc123"));
+        assertRedirect(client, getLocation(baseUri, "/ui/query.html", "abc123"), getLocation(baseUri, "/ui/login.html", "/ui/query.html?abc123"), false);
 
         assertResponseCode(client, getValidApiLocation(baseUri), SC_UNAUTHORIZED);
 
@@ -303,15 +303,15 @@ public void testNoPasswordAuthenticator()
     private void testNoPasswordAuthenticator(URI baseUri)
             throws Exception
     {
-        assertRedirect(client, getUiLocation(baseUri), getDisabledLocation(baseUri));
+        assertRedirect(client, getUiLocation(baseUri), getDisabledLocation(baseUri), false);
 
-        assertRedirect(client, getLocation(baseUri, "/ui/query.html", "abc123"), getDisabledLocation(baseUri));
+        assertRedirect(client, getLocation(baseUri, "/ui/query.html", "abc123"), getDisabledLocation(baseUri), false);
 
         assertResponseCode(client, getValidApiLocation(baseUri), SC_UNAUTHORIZED);
 
-        assertRedirect(client, getLoginLocation(baseUri), getDisabledLocation(baseUri));
+        assertRedirect(client, getLoginLocation(baseUri), getDisabledLocation(baseUri), false);
 
-        assertRedirect(client, getLogoutLocation(baseUri), getDisabledLocation(baseUri));
+        assertRedirect(client, getLogoutLocation(baseUri), getDisabledLocation(baseUri), false);
 
         assertOk(client, getValidAssetsLocation(baseUri));
 
@@ -344,13 +344,115 @@ private static void assertRedirect(OkHttpClient client, String url, String redir
         if (testProxy) {
             request = new Request.Builder()
                     .url(url)
-                    .header(X_FORWARDED_PROTO, "https")
+                    .header(X_FORWARDED_PROTO, "test")
+                    .header(X_FORWARDED_HOST, "my-load-balancer.local")
+                    .header(X_FORWARDED_PORT, "123")
+                    .build();
+            try (Response response = client.newCall(request).execute()) {
+                assertEquals(response.code(), SC_SEE_OTHER);
+                assertEquals(
+                        response.header(LOCATION),
+                        uriBuilderFrom(URI.create(redirectLocation))
+                                .scheme("test")
+                                .host("my-load-balancer.local")
+                                .port(123)
+                                .toString());
+            }
+
+            request = new Request.Builder()
+                    .url(url)
+                    .header(X_FORWARDED_PROTO, "test")
+                    .header(X_FORWARDED_HOST, "my-load-balancer.local:123")
+                    .build();
+            try (Response response = client.newCall(request).execute()) {
+                assertEquals(response.code(), SC_SEE_OTHER);
+                assertEquals(
+                        response.header(LOCATION),
+                        uriBuilderFrom(URI.create(redirectLocation))
+                                .scheme("test")
+                                .host("my-load-balancer.local")
+                                .port(123)
+                                .toString());
+            }
+
+            request = new Request.Builder()
+                    .url(url)
+                    .header(X_FORWARDED_PROTO, "test")
+                    .header(X_FORWARDED_PORT, "123")
+                    .build();
+            try (Response response = client.newCall(request).execute()) {
+                assertEquals(response.code(), SC_SEE_OTHER);
+                assertEquals(
+                        response.header(LOCATION),
+                        uriBuilderFrom(URI.create(redirectLocation))
+                                .scheme("test")
+                                .port(123)
+                                .toString());
+            }
+
+            request = new Request.Builder()
+                    .url(url)
+                    .header(X_FORWARDED_PROTO, "test")
+                    .build();
+            try (Response response = client.newCall(request).execute()) {
+                assertEquals(response.code(), SC_SEE_OTHER);
+                assertEquals(
+                        response.header(LOCATION),
+                        uriBuilderFrom(URI.create(redirectLocation))
+                                .scheme("test")
+                                .toString());
+            }
+            request = new Request.Builder()
+                    .url(url)
                     .header(X_FORWARDED_HOST, "my-load-balancer.local")
-                    .header(X_FORWARDED_PORT, "443")
+                    .header(X_FORWARDED_PORT, "123")
+                    .build();
+            try (Response response = client.newCall(request).execute()) {
+                assertEquals(response.code(), SC_SEE_OTHER);
+                assertEquals(
+                        response.header(LOCATION),
+                        uriBuilderFrom(URI.create(redirectLocation))
+                                .host("my-load-balancer.local")
+                                .port(123)
+                                .toString());
+            }
+
+            request = new Request.Builder()
+                    .url(url)
+                    .header(X_FORWARDED_HOST, "my-load-balancer.local:123")
+                    .build();
+            try (Response response = client.newCall(request).execute()) {
+                assertEquals(response.code(), SC_SEE_OTHER);
+                assertEquals(
+                        response.header(LOCATION),
+                        uriBuilderFrom(URI.create(redirectLocation))
+                                .host("my-load-balancer.local")
+                                .port(123)
+                                .toString());
+            }
+
+            request = new Request.Builder()
+                    .url(url)
+                    .header(X_FORWARDED_HOST, "my-load-balancer.local")
+                    .build();
+            try (Response response = client.newCall(request).execute()) {
+                assertEquals(response.code(), SC_SEE_OTHER);
+                assertEquals(
+                        response.header(LOCATION),
+                        uriBuilderFrom(URI.create(redirectLocation))
+                                .host("my-load-balancer.local")
+                                .defaultPort()
+                                .toString());
+            }
+
+            // X-Forwarded-Port not recognized as valid forwarding
+            request = new Request.Builder()
+                    .url(url)
+                    .header(X_FORWARDED_PORT, "123")
                     .build();
             try (Response response = client.newCall(request).execute()) {
                 assertEquals(response.code(), SC_SEE_OTHER);
-                assertEquals(response.header(LOCATION), "https://my-load-balancer.local:443/" + redirectLocation.replaceFirst("^([^/]*/){3}", ""));
+                assertEquals(response.header(LOCATION), redirectLocation);
             }
         }
     }