You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@asterixdb.apache.org by AsterixDB Code Review <do...@asterix-gerrit.ics.uci.edu> on 2026/09/30 08:25:26 UTC

[M] Change in asterixdb[master]: [NO ISSUE][API] Use request scheme in result handle URLs

From Murtadha Hubail <mh...@apache.org>:

Murtadha Hubail has uploaded this change for review. ( https://asterix-gerrit.ics.uci.edu/c/asterixdb/+/21948?usp=email )


Change subject: [NO ISSUE][API] Use request scheme in result handle URLs
......................................................................

[NO ISSUE][API] Use request scheme in result handle URLs

Host-qualified result and status handles were always built with
"http://", while the host and port are taken from the request. A request
received over TLS therefore got handles pointing plaintext at the TLS
port, which cannot be fetched as returned.

Ext-ref: MB-74299
Generated-by: Claude Opus 5.5 (1M context) <no...@anthropic.com>
Change-Id: If97b78f5899546195ec13ff8358433cc7df56270
---
M asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/NCQueryServiceServlet.java
M asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/QueryServiceRequestParameters.java
M asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/QueryServiceServlet.java
M asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/QueryStatusApiServlet.java
A asterixdb/asterix-app/src/test/java/org/apache/asterix/api/http/server/ResultHandleSchemeTest.java
5 files changed, 155 insertions(+), 6 deletions(-)



  git pull ssh://asterix-gerrit.ics.uci.edu:29418/asterixdb refs/changes/48/21948/1

diff --git a/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/NCQueryServiceServlet.java b/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/NCQueryServiceServlet.java
index e43da3f..ddb67bf 100644
--- a/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/NCQueryServiceServlet.java
+++ b/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/NCQueryServiceServlet.java
@@ -102,7 +102,7 @@
         if (!param.isIncludeHost()) {
             handleUrl = getHandleUrl(param.getPath(), delivery);
         } else {
-            handleUrl = getHandleUrl(param.getHost(), param.getPath(), delivery);
+            handleUrl = getHandleUrl(param.getScheme(), param.getHost(), param.getPath(), delivery);
         }
         try {
             long timeout = param.getTimeout();
diff --git a/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/QueryServiceRequestParameters.java b/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/QueryServiceRequestParameters.java
index 57fc8d7..9d6585a 100644
--- a/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/QueryServiceRequestParameters.java
+++ b/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/QueryServiceRequestParameters.java
@@ -55,6 +55,7 @@
 import com.google.common.collect.ImmutableMap;
 
 import io.netty.handler.codec.http.HttpMethod;
+import io.netty.handler.codec.http.HttpScheme;
 
 public class QueryServiceRequestParameters {
 
@@ -128,6 +129,7 @@
             ImmutableMap.of("present", Boolean.TRUE, "absent", Boolean.FALSE);
     private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
 
+    private HttpScheme scheme = HttpScheme.HTTP;
     private String host;
     private String path;
     private String statement;
@@ -163,6 +165,14 @@
     private long resultTtlInMillis = -1L; // -1 means use system default
     private boolean includeHost = false;
 
+    public HttpScheme getScheme() {
+        return scheme;
+    }
+
+    public void setScheme(HttpScheme scheme) {
+        this.scheme = scheme;
+    }
+
     public String getHost() {
         return host;
     }
@@ -495,6 +505,7 @@
 
     public void setParameters(QueryServiceServlet servlet, IServletRequest request, Map<String, String> optionalParams)
             throws IOException {
+        setScheme(request.getScheme());
         setHost(servlet.host(request));
         setPath(servlet.servletPath(request));
         setOptionalParams(optionalParams);
diff --git a/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/QueryServiceServlet.java b/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/QueryServiceServlet.java
index ccddc13..ba2a026 100644
--- a/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/QueryServiceServlet.java
+++ b/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/QueryServiceServlet.java
@@ -90,6 +90,7 @@
 import org.apache.logging.log4j.Logger;
 
 import io.netty.handler.codec.http.HttpResponseStatus;
+import io.netty.handler.codec.http.HttpScheme;
 
 public class QueryServiceServlet extends AbstractQueryApiServlet {
     protected static final Logger LOGGER = LogManager.getLogger();
@@ -200,8 +201,8 @@
         return path + handlePath(delivery);
     }
 
-    protected String getHandleUrl(String host, String path, ResultDelivery delivery) {
-        return "http://" + host + path + handlePath(delivery);
+    protected String getHandleUrl(HttpScheme scheme, String host, String path, ResultDelivery delivery) {
+        return scheme + "://" + host + path + handlePath(delivery);
     }
 
     private void handleRequest(IServletRequest request, IServletResponse response, boolean forceReadOnly)
@@ -444,8 +445,9 @@
 
     private void setSessionConfig(SessionOutput sessionOutput, QueryServiceRequestParameters param,
             ResultDelivery delivery) {
-        String handleUrl = param.isIncludeHost() ? getHandleUrl(param.getHost(), param.getPath(), delivery)
-                : getHandleUrl(param.getPath(), delivery);
+        String handleUrl =
+                param.isIncludeHost() ? getHandleUrl(param.getScheme(), param.getHost(), param.getPath(), delivery)
+                        : getHandleUrl(param.getPath(), delivery);
         sessionOutput.setHandleAppender(ResultUtil.createResultHandleAppender(handleUrl));
         SessionConfig sessionConfig = sessionOutput.config();
         SessionConfig.ClientType clientType = param.getClientType();
diff --git a/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/QueryStatusApiServlet.java b/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/QueryStatusApiServlet.java
index e5b9df3..277b997 100644
--- a/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/QueryStatusApiServlet.java
+++ b/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/QueryStatusApiServlet.java
@@ -122,7 +122,7 @@
         if (uriMode) {
             resHandle = servletPath + strHandle;
         } else {
-            resHandle = "http://" + host(request) + servletPath + strHandle;
+            resHandle = request.getScheme() + "://" + host(request) + servletPath + strHandle;
         }
         printer.addResultPrinter(new ResultHandlePrinter(resHandle));
         if (uriMode) {
diff --git a/asterixdb/asterix-app/src/test/java/org/apache/asterix/api/http/server/ResultHandleSchemeTest.java b/asterixdb/asterix-app/src/test/java/org/apache/asterix/api/http/server/ResultHandleSchemeTest.java
new file mode 100644
index 0000000..92e0b46
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/java/org/apache/asterix/api/http/server/ResultHandleSchemeTest.java
@@ -0,0 +1,136 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.asterix.api.http.server;
+
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.asterix.api.common.AsterixHyracksIntegrationUtil;
+import org.apache.commons.io.IOUtils;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpRequestBase;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.hyracks.http.server.utils.HttpUtil;
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * A host-qualified result handle must carry the scheme the request arrived on, not only its host and port: a handle
+ * naming the TLS port under {@code http://} cannot be fetched as returned. The request's scheme is resolved the same
+ * way for a TLS listener and for a request forwarded by a proxy that terminated TLS, so the forwarded form stands in
+ * for a TLS cluster here.
+ */
+public class ResultHandleSchemeTest {
+
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+    private static final String CC_ENDPOINT = "localhost:19002";
+    private static final String NC_ENDPOINT = "localhost:19004";
+    private static final long STATUS_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(60);
+
+    private static final AsterixHyracksIntegrationUtil INTEGRATION_UTIL = new AsterixHyracksIntegrationUtil();
+
+    @BeforeClass
+    public static void setUp() throws Exception {
+        INTEGRATION_UTIL.init(true, AsterixHyracksIntegrationUtil.DEFAULT_CONF_FILE);
+    }
+
+    @AfterClass
+    public static void tearDown() throws Exception {
+        INTEGRATION_UTIL.deinit(true);
+    }
+
+    @Test
+    public void deferredHandleFromTheCcKeepsTheRequestScheme() throws Exception {
+        assertHandle(submit(CC_ENDPOINT, "deferred", "https"), "https", CC_ENDPOINT, "/query/service/result/");
+    }
+
+    @Test
+    public void deferredHandleFromAnNcKeepsTheRequestScheme() throws Exception {
+        assertHandle(submit(NC_ENDPOINT, "deferred", "https"), "https", NC_ENDPOINT, "/query/service/result/");
+    }
+
+    @Test
+    public void resultHandleFromAsyncStatusKeepsTheRequestScheme() throws Exception {
+        String statusHandle = submit(CC_ENDPOINT, "async", "https");
+        assertHandle(statusHandle, "https", CC_ENDPOINT, "/query/service/status/");
+        assertHandle(awaitResultHandle(statusHandle, "https"), "https", CC_ENDPOINT, "/query/service/result/");
+    }
+
+    @Test
+    public void plainRequestStillGetsAnHttpHandle() throws Exception {
+        assertHandle(submit(CC_ENDPOINT, "deferred", null), "http", CC_ENDPOINT, "/query/service/result/");
+    }
+
+    private static void assertHandle(String handle, String scheme, String endpoint, String path) {
+        Assert.assertTrue(handle, handle.startsWith(scheme + "://" + endpoint + path));
+    }
+
+    private static String submit(String endpoint, String mode, String forwardedProto) throws Exception {
+        HttpPost request = new HttpPost("http://" + endpoint + "/query/service");
+        request.setEntity(new UrlEncodedFormEntity(
+                List.of(new BasicNameValuePair("statement", "select 1;"), new BasicNameValuePair("mode", mode)),
+                StandardCharsets.UTF_8));
+        return handleOf(execute(request, forwardedProto));
+    }
+
+    private static String awaitResultHandle(String statusHandle, String forwardedProto) throws Exception {
+        // the server only listens on plain HTTP; the forwarded header is what makes the request a TLS one
+        String statusUrl = statusHandle.replaceFirst("^https://", "http://");
+        long deadline = System.currentTimeMillis() + STATUS_TIMEOUT_MILLIS;
+        while (true) {
+            JsonNode status = execute(new HttpGet(statusUrl), forwardedProto);
+            if ("success".equals(status.path("status").asText())) {
+                return handleOf(status);
+            }
+            if (System.currentTimeMillis() > deadline) {
+                Assert.fail("request did not succeed in time: " + status);
+            }
+            TimeUnit.MILLISECONDS.sleep(100);
+        }
+    }
+
+    private static String handleOf(JsonNode response) {
+        JsonNode handle = response.get("handle");
+        Assert.assertNotNull(response.toString(), handle);
+        return handle.asText();
+    }
+
+    private static JsonNode execute(HttpRequestBase request, String forwardedProto) throws Exception {
+        if (forwardedProto != null) {
+            request.setHeader(HttpUtil.X_FORWARDED_PROTO.toString(), forwardedProto);
+        }
+        try (CloseableHttpClient httpClient = HttpClients.createDefault();
+                CloseableHttpResponse response = httpClient.execute(request)) {
+            String body = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
+            Assert.assertEquals(body, 200, response.getStatusLine().getStatusCode());
+            return OBJECT_MAPPER.readTree(body);
+        }
+    }
+}

-- 
To view, visit https://asterix-gerrit.ics.uci.edu/c/asterixdb/+/21948?usp=email
To unsubscribe, or for help writing mail filters, visit https://asterix-gerrit.ics.uci.edu/settings?usp=email

Gerrit-MessageType: newchange
Gerrit-Project: asterixdb
Gerrit-Branch: master
Gerrit-Change-Id: If97b78f5899546195ec13ff8358433cc7df56270
Gerrit-Change-Number: 21948
Gerrit-PatchSet: 1
Gerrit-Owner: Murtadha Hubail <mh...@apache.org>