diff --git a/docker/configs/server1-conf/rest-server.properties b/docker/configs/server1-conf/rest-server.properties index 1fd064d88a..fce537bb1c 100644 --- a/docker/configs/server1-conf/rest-server.properties +++ b/docker/configs/server1-conf/rest-server.properties @@ -1,7 +1,7 @@ # bind url -restserver.url=http://127.0.0.1:8081 +restserver.url=127.0.0.1:8081 # gremlin server url, need to be consistent with host and port in gremlin-server.yaml -gremlinserver.url=http://127.0.0.1:8181 +gremlinserver.url=127.0.0.1:8181 graphs=./conf/graphs diff --git a/docker/configs/server2-conf/rest-server.properties b/docker/configs/server2-conf/rest-server.properties index cff9405f5c..0e296b17b4 100644 --- a/docker/configs/server2-conf/rest-server.properties +++ b/docker/configs/server2-conf/rest-server.properties @@ -1,7 +1,7 @@ # bind url -restserver.url=http://127.0.0.1:8082 +restserver.url=127.0.0.1:8082 # gremlin server url, need to be consistent with host and port in gremlin-server.yaml -gremlinserver.url=http://127.0.0.1:8182 +gremlinserver.url=127.0.0.1:8182 graphs=./conf/graphs diff --git a/docker/configs/server3-conf/rest-server.properties b/docker/configs/server3-conf/rest-server.properties index 6c158e6236..f628dc61b4 100644 --- a/docker/configs/server3-conf/rest-server.properties +++ b/docker/configs/server3-conf/rest-server.properties @@ -1,7 +1,7 @@ # bind url -restserver.url=http://127.0.0.1:8083 +restserver.url=127.0.0.1:8083 # gremlin server url, need to be consistent with host and port in gremlin-server.yaml -gremlinserver.url=http://127.0.0.1:8183 +gremlinserver.url=127.0.0.1:8183 graphs=./conf/graphs diff --git a/hugegraph-commons/hugegraph-common/src/main/java/org/apache/hugegraph/config/ConfigOption.java b/hugegraph-commons/hugegraph-common/src/main/java/org/apache/hugegraph/config/ConfigOption.java index 159f13901f..cffef28f19 100644 --- a/hugegraph-commons/hugegraph-common/src/main/java/org/apache/hugegraph/config/ConfigOption.java +++ b/hugegraph-commons/hugegraph-common/src/main/java/org/apache/hugegraph/config/ConfigOption.java @@ -20,6 +20,22 @@ import com.google.common.base.Predicate; public class ConfigOption extends TypedOption { + private boolean urlNormalize = false; + private String defaultScheme = null; + + public ConfigOption withUrlNormalization(String scheme) { + this.urlNormalize = true; + this.defaultScheme = scheme; + return this; + } + + public boolean needsUrlNormalization() { + return this.urlNormalize; + } + + public String getDefaultScheme() { + return this.defaultScheme; + } public ConfigOption(String name, String desc, T value) { this(name, desc, null, value); diff --git a/hugegraph-commons/hugegraph-common/src/main/java/org/apache/hugegraph/config/HugeConfig.java b/hugegraph-commons/hugegraph-common/src/main/java/org/apache/hugegraph/config/HugeConfig.java index 4837154563..9f9ecdea4d 100644 --- a/hugegraph-commons/hugegraph-common/src/main/java/org/apache/hugegraph/config/HugeConfig.java +++ b/hugegraph-commons/hugegraph-common/src/main/java/org/apache/hugegraph/config/HugeConfig.java @@ -43,6 +43,9 @@ public class HugeConfig extends PropertiesConfiguration { private static final Logger LOG = Log.logger(HugeConfig.class); + // Cache for URL normalization metadata (populated lazily per key) + private static final Map URL_NORMALIZATIONS = new HashMap<>(); + private String configPath; public HugeConfig(Configuration config) { @@ -87,9 +90,17 @@ private void setLayoutIfNeeded(Configuration conf) { @SuppressWarnings("unchecked") public R get(TypedOption option) { Object value = this.getProperty(option.name()); + boolean fromDefault = false; + if (value == null) { - return option.defaultValue(); + value = option.defaultValue(); + fromDefault = true; } + + if (!fromDefault) { + value = normalizeUrlOptionIfNeeded(option.name(), value); + } + return (R) value; } @@ -213,4 +224,86 @@ private static Configuration loadConfigFile(File configFile) { e, configFile); } } + + private static Object normalizeUrlOptionIfNeeded(String key, Object value) { + if (value == null) { + return null; + } + + String scheme = defaultSchemeFor(key); + if (scheme == null) { + return value; + } + + // Normalize URL options if configured with .withUrlNormalization() + if (value instanceof String) { + String original = (String) value; + String normalized = prefixSchemeIfMissing(original, scheme); + + if (!original.equals(normalized)) { + LOG.warn("Config '{}' is missing scheme, auto-corrected to '{}'", + key, normalized); + } + + return normalized; + } + + // If it ever hits here, it means config storage returned a non-string type; + // leave it unchanged (safer than forcing toString()). + return value; + } + + private static String defaultSchemeFor(String key) { + // Check if we already cached this key's scheme + if (URL_NORMALIZATIONS.containsKey(key)) { + return URL_NORMALIZATIONS.get(key); + } + + // We don't know yet - look it up NOW from OptionSpace + synchronized (URL_NORMALIZATIONS) { + // Double-check after acquiring lock + if (URL_NORMALIZATIONS.containsKey(key)) { + return URL_NORMALIZATIONS.get(key); + } + + // Look up the option from OptionSpace + TypedOption option = OptionSpace.get(key); + String scheme = null; + + if (option instanceof ConfigOption) { + ConfigOption configOption = (ConfigOption) option; + if (configOption.needsUrlNormalization()) { + scheme = configOption.getDefaultScheme(); + } + } + + // Cache it for next time (even if null) + URL_NORMALIZATIONS.put(key, scheme); + return scheme; + } + } + + private static String prefixSchemeIfMissing(String raw, String scheme) { + if (raw == null) { + return null; + } + String s = raw.trim(); + if (s.isEmpty()) { + return s; + } + + int scIdx = s.indexOf("://"); + if (scIdx > 0) { + // Normalize existing scheme to lowercase while preserving the rest + String existingScheme = s.substring(0, scIdx).toLowerCase(); + String rest = s.substring(scIdx + 3); // skip the "://" delimiter + return existingScheme + "://" + rest; + } + + String defaultScheme = scheme == null ? "" : scheme; + if (!defaultScheme.isEmpty() && !defaultScheme.endsWith("://")) { + defaultScheme = defaultScheme + "://"; + } + return defaultScheme + s; + } } diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/config/ServerOptions.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/config/ServerOptions.java index 920d119d45..278542854b 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/config/ServerOptions.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/config/ServerOptions.java @@ -32,7 +32,7 @@ public class ServerOptions extends OptionHolder { "The url for listening of graph server.", disallowEmpty(), "http://127.0.0.1:8080" - ); + ).withUrlNormalization("http://"); public static final ConfigOption SERVER_EVENT_HUB_THREADS = new ConfigOption<>( @@ -118,7 +118,7 @@ public class ServerOptions extends OptionHolder { "The url of gremlin server.", disallowEmpty(), "http://127.0.0.1:8182" - ); + ).withUrlNormalization("http://"); public static final ConfigOption GREMLIN_SERVER_TIMEOUT = new ConfigOption<>( @@ -270,7 +270,7 @@ public class ServerOptions extends OptionHolder { "to clients. only used when starting the server in k8s.", disallowEmpty(), "http://0.0.0.0:8080" - ); + ).withUrlNormalization("http://"); public static final ConfigOption SERVER_K8S_URL = new ConfigOption<>( @@ -278,7 +278,7 @@ public class ServerOptions extends OptionHolder { "The url of k8s.", disallowEmpty(), "https://127.0.0.1:8888" - ); + ).withUrlNormalization("https://"); public static final ConfigOption SERVER_K8S_USE_CA = new ConfigOption<>( diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/conf/rest-server.properties b/hugegraph-server/hugegraph-dist/src/assembly/static/conf/rest-server.properties index 0dce972719..ad3e2700f8 100644 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/conf/rest-server.properties +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/conf/rest-server.properties @@ -1,9 +1,9 @@ # bind url # could use '0.0.0.0' or specified (real)IP to expose external network access -restserver.url=http://127.0.0.1:8080 +restserver.url=127.0.0.1:8080 #restserver.enable_graphspaces_filter=false # gremlin server url, need to be consistent with host and port in gremlin-server.yaml -#gremlinserver.url=http://127.0.0.1:8182 +#gremlinserver.url=127.0.0.1:8182 graphs=./conf/graphs diff --git a/hugegraph-server/hugegraph-test/src/test/java/org/apache/hugegraph/unit/config/ServerOptionsTest.java b/hugegraph-server/hugegraph-test/src/test/java/org/apache/hugegraph/unit/config/ServerOptionsTest.java new file mode 100644 index 0000000000..6113c748b0 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/test/java/org/apache/hugegraph/unit/config/ServerOptionsTest.java @@ -0,0 +1,122 @@ +/* + * 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.hugegraph.unit.config; + +import org.apache.commons.configuration2.PropertiesConfiguration; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.config.OptionSpace; +import org.apache.hugegraph.config.ServerOptions; +import org.apache.hugegraph.testutil.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +public class ServerOptionsTest { + + @BeforeClass + public static void init() { + OptionSpace.register("server", + ServerOptions.class.getName()); + } + + @Test + public void testUrlOptionNormalizeAddsDefaultScheme() { + PropertiesConfiguration conf = new PropertiesConfiguration(); + conf.setProperty("restserver.url", "127.0.0.1:8080"); + conf.setProperty("gremlinserver.url", "127.0.0.1:8182"); + conf.setProperty("server.urls_to_pd", "0.0.0.0:8080"); + conf.setProperty("server.k8s_url", "127.0.0.1:8888"); + + HugeConfig config = new HugeConfig(conf); + + Assert.assertEquals("http://127.0.0.1:8080", + config.get(ServerOptions.REST_SERVER_URL)); + Assert.assertEquals("http://127.0.0.1:8182", + config.get(ServerOptions.GREMLIN_SERVER_URL)); + Assert.assertEquals("http://0.0.0.0:8080", + config.get(ServerOptions.SERVER_URLS_TO_PD)); + Assert.assertEquals("https://127.0.0.1:8888", + config.get(ServerOptions.SERVER_K8S_URL)); + } + + @Test + public void testUrlNormalizationEdgeCases() { + // Whitespace trimming + PropertiesConfiguration conf = new PropertiesConfiguration(); + conf.setProperty("restserver.url", " 127.0.0.1:8080 "); + HugeConfig config = new HugeConfig(conf); + Assert.assertEquals("http://127.0.0.1:8080", + config.get(ServerOptions.REST_SERVER_URL)); + + // Case normalization + conf = new PropertiesConfiguration(); + conf.setProperty("restserver.url", "HTTP://127.0.0.1:8080"); + config = new HugeConfig(conf); + Assert.assertEquals("http://127.0.0.1:8080", + config.get(ServerOptions.REST_SERVER_URL)); + + // IPv6 without scheme + conf = new PropertiesConfiguration(); + conf.setProperty("restserver.url", "[::1]:8080"); + config = new HugeConfig(conf); + Assert.assertEquals("http://[::1]:8080", + config.get(ServerOptions.REST_SERVER_URL)); + + // IPv6 with existing scheme + conf = new PropertiesConfiguration(); + conf.setProperty("restserver.url", "http://[::1]:8080"); + config = new HugeConfig(conf); + Assert.assertEquals("http://[::1]:8080", + config.get(ServerOptions.REST_SERVER_URL)); + } + + @Test + public void testUrlNormalizationPreservesHostnameCase() { + // Uppercase scheme + mixed-case hostname + PropertiesConfiguration conf = new PropertiesConfiguration(); + conf.setProperty("restserver.url", "HTTP://MyServer:8080"); + HugeConfig config = new HugeConfig(conf); + // Should lowercase ONLY the scheme, preserve "MyServer" + Assert.assertEquals("http://MyServer:8080", + config.get(ServerOptions.REST_SERVER_URL)); + + // Use server.k8s_url for HTTPS test (it defaults to https://) + conf = new PropertiesConfiguration(); + conf.setProperty("server.k8s_url", "HTTPS://MyHost:8888"); + config = new HugeConfig(conf); + Assert.assertEquals("https://MyHost:8888", + config.get(ServerOptions.SERVER_K8S_URL)); + } + + @Test + public void testUrlNormalizationPreservesPathCase() { + PropertiesConfiguration conf = new PropertiesConfiguration(); + conf.setProperty("restserver.url", "http://127.0.0.1:8080/SomePath/CaseSensitive"); + HugeConfig config = new HugeConfig(conf); + Assert.assertEquals("http://127.0.0.1:8080/SomePath/CaseSensitive", + config.get(ServerOptions.REST_SERVER_URL)); + } + + @Test + public void testHttpsSchemeIsNotDowngraded() { + PropertiesConfiguration conf = new PropertiesConfiguration(); + conf.setProperty("restserver.url", "https://127.0.0.1:8080"); + HugeConfig config = new HugeConfig(conf); + Assert.assertEquals("https://127.0.0.1:8080", + config.get(ServerOptions.REST_SERVER_URL)); + } +}