This is an automated email from the ASF dual-hosted git repository.
sdanilov pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git
The following commit(s) were added to refs/heads/main by this push:
new 1d3924acb6 IGNITE-18577: Add SSL support for the REST (#1632)
1d3924acb6 is described below
commit 1d3924acb6072519e202be3a82fe91bc893c0121
Author: Ivan Gagarkin <[email protected]>
AuthorDate: Wed Feb 8 14:54:21 2023 +0400
IGNITE-18577: Add SSL support for the REST (#1632)
---
.../org/apache/ignite/network/NodeMetadata.java | 32 +++-
.../internal/cli/commands/ItConfigCommandTest.java | 10 +-
.../repl/registry/impl/NodeNameRegistryImpl.java | 2 +-
.../management/rest/TopologyController.java | 2 +-
.../network/scalecube/ItClusterServiceTest.java | 4 +-
.../KeyStoreConfigurationSchema.java} | 27 ++--
.../KeyStoreConfigurationValidator.java} | 27 ++--
.../KeyStoreConfigurationValidatorImpl.java | 57 +++++++
.../KeyStoreConfigurationValidatorImplTest.java | 77 +++++++++
.../network/configuration/StubKeyStoreView.java} | 41 +++--
.../internal/rest/api/cluster/NodeMetadataDto.java | 34 ++--
modules/rest/build.gradle | 1 +
modules/rest/openapi/openapi.yaml | 8 +-
.../apache/ignite/internal/rest/RestComponent.java | 141 +++++++++++++----
.../configuration/RestConfigurationModule.java | 8 +
.../configuration/RestConfigurationSchema.java | 13 ++
...Schema.java => RestSslConfigurationSchema.java} | 31 ++--
.../ignite/internal/rest/ItPortRangeTest.java | 130 +++++++++++++++
.../ignite/internal/rest/ssl/ItRestSslTest.java | 175 +++++++++++++++++++++
.../apache/ignite/internal/rest/ssl/RestNode.java | 99 ++++++++++++
.../src/integrationTest/resources/ssl/keystore.p12 | Bin 0 -> 4286 bytes
.../integrationTest/resources/ssl/truststore.jks | Bin 0 -> 1558 bytes
.../org/apache/ignite/internal/app/IgniteImpl.java | 35 ++++-
.../internal/component/RestAddressReporter.java | 23 ++-
.../component/RestAddressReporterTest.java | 41 ++++-
packaging/zip/ignite3db | 2 +-
26 files changed, 889 insertions(+), 131 deletions(-)
diff --git
a/modules/api/src/main/java/org/apache/ignite/network/NodeMetadata.java
b/modules/api/src/main/java/org/apache/ignite/network/NodeMetadata.java
index 790ad4ba3c..154c602d2c 100644
--- a/modules/api/src/main/java/org/apache/ignite/network/NodeMetadata.java
+++ b/modules/api/src/main/java/org/apache/ignite/network/NodeMetadata.java
@@ -28,19 +28,27 @@ public class NodeMetadata implements Serializable {
private final String restHost;
- private final int restPort;
+ private final int httpPort;
- public NodeMetadata(String restHost, int restPort) {
+ private final int httpsPort;
+
+ /** Constructor. */
+ public NodeMetadata(String restHost, int httpPort, int httpsPort) {
this.restHost = restHost;
- this.restPort = restPort;
+ this.httpPort = httpPort;
+ this.httpsPort = httpsPort;
}
public String restHost() {
return restHost;
}
- public int restPort() {
- return restPort;
+ public int httpPort() {
+ return httpPort;
+ }
+
+ public int httpsPort() {
+ return httpsPort;
}
@Override
@@ -48,15 +56,23 @@ public class NodeMetadata implements Serializable {
if (this == o) {
return true;
}
- if (!(o instanceof NodeMetadata)) {
+ if (o == null || getClass() != o.getClass()) {
return false;
}
+
NodeMetadata that = (NodeMetadata) o;
- return restPort == that.restPort && Objects.equals(restHost,
that.restHost);
+
+ if (httpPort != that.httpPort) {
+ return false;
+ }
+ if (httpsPort != that.httpsPort) {
+ return false;
+ }
+ return restHost != null ? restHost.equals(that.restHost) :
that.restHost == null;
}
@Override
public int hashCode() {
- return Objects.hash(restHost, restPort);
+ return Objects.hash(restHost, httpPort, httpsPort);
}
}
diff --git
a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/ItConfigCommandTest.java
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/ItConfigCommandTest.java
index 024b027c4b..77fed750a4 100644
---
a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/ItConfigCommandTest.java
+++
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/ItConfigCommandTest.java
@@ -73,7 +73,7 @@ public class ItConfigCommandTest extends AbstractCliTest {
"config",
"update",
"--node-url",
- "http://localhost:" + node.restAddress().port(),
+ "http://localhost:" + node.restHttpAddress().port(),
"network.shutdownQuietPeriod=1"
);
@@ -91,7 +91,7 @@ public class ItConfigCommandTest extends AbstractCliTest {
"config",
"show",
"--node-url",
- "http://localhost:" + node.restAddress().port()
+ "http://localhost:" + node.restHttpAddress().port()
);
assertEquals(0, exitCode);
@@ -109,7 +109,7 @@ public class ItConfigCommandTest extends AbstractCliTest {
"config",
"update",
"--node-url",
- "http://localhost:" + node.restAddress().port(),
+ "http://localhost:" + node.restHttpAddress().port(),
"network.foo=\"bar\""
);
@@ -126,7 +126,7 @@ public class ItConfigCommandTest extends AbstractCliTest {
"config",
"update",
"--node-url",
- "http://localhost:" + node.restAddress().port(),
+ "http://localhost:" + node.restHttpAddress().port(),
"network.shutdownQuietPeriod=asd"
);
@@ -144,7 +144,7 @@ public class ItConfigCommandTest extends AbstractCliTest {
"config",
"show",
"--node-url",
- "http://localhost:" + node.restAddress().port(),
+ "http://localhost:" + node.restHttpAddress().port(),
"network"
);
diff --git
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/repl/registry/impl/NodeNameRegistryImpl.java
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/repl/registry/impl/NodeNameRegistryImpl.java
index 72a048b13b..e0de828d6a 100644
---
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/repl/registry/impl/NodeNameRegistryImpl.java
+++
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/repl/registry/impl/NodeNameRegistryImpl.java
@@ -94,7 +94,7 @@ public class NodeNameRegistryImpl implements
NodeNameRegistry, AsyncSessionEvent
return null;
}
try {
- return new URL("http://" + metadata.getRestHost() + ":" +
metadata.getRestPort());
+ return new URL("http://" + metadata.getRestHost() + ":" +
metadata.getHttpPort());
} catch (Exception e) {
log.warn("Couldn't create URL: {}", e);
return null;
diff --git
a/modules/cluster-management/src/main/java/org/apache/ignite/internal/cluster/management/rest/TopologyController.java
b/modules/cluster-management/src/main/java/org/apache/ignite/internal/cluster/management/rest/TopologyController.java
index 52688aafc2..e926f7ad3f 100644
---
a/modules/cluster-management/src/main/java/org/apache/ignite/internal/cluster/management/rest/TopologyController.java
+++
b/modules/cluster-management/src/main/java/org/apache/ignite/internal/cluster/management/rest/TopologyController.java
@@ -87,6 +87,6 @@ public class TopologyController implements TopologyApi {
if (metadata == null) {
return null;
}
- return new NodeMetadataDto(metadata.restHost(), metadata.restPort());
+ return new NodeMetadataDto(metadata.restHost(), metadata.httpPort(),
metadata.httpsPort());
}
}
diff --git
a/modules/network/src/integrationTest/java/org/apache/ignite/network/scalecube/ItClusterServiceTest.java
b/modules/network/src/integrationTest/java/org/apache/ignite/network/scalecube/ItClusterServiceTest.java
index c6400f789c..089736c8e5 100644
---
a/modules/network/src/integrationTest/java/org/apache/ignite/network/scalecube/ItClusterServiceTest.java
+++
b/modules/network/src/integrationTest/java/org/apache/ignite/network/scalecube/ItClusterServiceTest.java
@@ -82,8 +82,8 @@ public class ItClusterServiceTest {
assertTrue(waitForCondition(() ->
service2.topologyService().allMembers().size() == 2, 1000));
try {
assertThat(service1.topologyService().localMember().nodeMetadata(),
is(nullValue()));
- var meta1 = new NodeMetadata("foo", 123);
- var meta2 = new NodeMetadata("bar", 456);
+ var meta1 = new NodeMetadata("foo", 123, 321);
+ var meta2 = new NodeMetadata("bar", 456, 654);
service1.updateMetadata(meta1);
service2.updateMetadata(meta2);
checkLocalMeta(service1, meta1);
diff --git
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationSchema.java
b/modules/network/src/main/java/org/apache/ignite/internal/network/configuration/KeyStoreConfigurationSchema.java
similarity index 59%
copy from
modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationSchema.java
copy to
modules/network/src/main/java/org/apache/ignite/internal/network/configuration/KeyStoreConfigurationSchema.java
index cabd1c5a6b..895fa204d9 100644
---
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationSchema.java
+++
b/modules/network/src/main/java/org/apache/ignite/internal/network/configuration/KeyStoreConfigurationSchema.java
@@ -15,26 +15,21 @@
* limitations under the License.
*/
-package org.apache.ignite.internal.rest.configuration;
+package org.apache.ignite.internal.network.configuration;
-import org.apache.ignite.configuration.annotation.ConfigurationRoot;
-import org.apache.ignite.configuration.annotation.ConfigurationType;
+import org.apache.ignite.configuration.annotation.Config;
import org.apache.ignite.configuration.annotation.Value;
-import org.apache.ignite.configuration.validation.Range;
-/**
- * Configuration schema for REST endpoint subtree.
- */
-@SuppressWarnings("PMD.UnusedPrivateField")
-@ConfigurationRoot(rootName = "rest", type = ConfigurationType.LOCAL)
-public class RestConfigurationSchema {
- /** TCP port. */
- @Range(min = 1024, max = 0xFFFF)
+/** Key store configuration. */
+@Config
+public class KeyStoreConfigurationSchema {
+
+ @Value(hasDefault = true)
+ public String type = "PKCS12";
+
@Value(hasDefault = true)
- public final int port = 10300;
+ public String path = "";
- /** TCP port range. */
- @Range(min = 0)
@Value(hasDefault = true)
- public final int portRange = 100;
+ public String password = "";
}
diff --git
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationSchema.java
b/modules/network/src/main/java/org/apache/ignite/internal/network/configuration/KeyStoreConfigurationValidator.java
similarity index 52%
copy from
modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationSchema.java
copy to
modules/network/src/main/java/org/apache/ignite/internal/network/configuration/KeyStoreConfigurationValidator.java
index cabd1c5a6b..e96b9bd2be 100644
---
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationSchema.java
+++
b/modules/network/src/main/java/org/apache/ignite/internal/network/configuration/KeyStoreConfigurationValidator.java
@@ -15,26 +15,17 @@
* limitations under the License.
*/
-package org.apache.ignite.internal.rest.configuration;
+package org.apache.ignite.internal.network.configuration;
-import org.apache.ignite.configuration.annotation.ConfigurationRoot;
-import org.apache.ignite.configuration.annotation.ConfigurationType;
-import org.apache.ignite.configuration.annotation.Value;
-import org.apache.ignite.configuration.validation.Range;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
/**
- * Configuration schema for REST endpoint subtree.
+ * Annotation to validate whole key store configuration.
*/
-@SuppressWarnings("PMD.UnusedPrivateField")
-@ConfigurationRoot(rootName = "rest", type = ConfigurationType.LOCAL)
-public class RestConfigurationSchema {
- /** TCP port. */
- @Range(min = 1024, max = 0xFFFF)
- @Value(hasDefault = true)
- public final int port = 10300;
-
- /** TCP port range. */
- @Range(min = 0)
- @Value(hasDefault = true)
- public final int portRange = 100;
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface KeyStoreConfigurationValidator {
}
diff --git
a/modules/network/src/main/java/org/apache/ignite/internal/network/configuration/KeyStoreConfigurationValidatorImpl.java
b/modules/network/src/main/java/org/apache/ignite/internal/network/configuration/KeyStoreConfigurationValidatorImpl.java
new file mode 100644
index 0000000000..61ffbc7c02
--- /dev/null
+++
b/modules/network/src/main/java/org/apache/ignite/internal/network/configuration/KeyStoreConfigurationValidatorImpl.java
@@ -0,0 +1,57 @@
+/*
+ * 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.ignite.internal.network.configuration;
+
+import static org.apache.ignite.internal.util.StringUtils.nullOrBlank;
+
+import org.apache.ignite.configuration.validation.ValidationContext;
+import org.apache.ignite.configuration.validation.ValidationIssue;
+import org.apache.ignite.configuration.validation.Validator;
+
+/**
+ * Key store configuration validator implementation.
+ */
+public class KeyStoreConfigurationValidatorImpl implements
Validator<KeyStoreConfigurationValidator, KeyStoreView> {
+
+ public static final KeyStoreConfigurationValidatorImpl INSTANCE = new
KeyStoreConfigurationValidatorImpl();
+
+ @Override
+ public void validate(KeyStoreConfigurationValidator annotation,
ValidationContext<KeyStoreView> ctx) {
+ KeyStoreView keyStore = ctx.getNewValue();
+ String type = keyStore.type();
+ String path = keyStore.path();
+ String password = keyStore.password();
+ if (nullOrBlank(path) && nullOrBlank(password)) {
+ return;
+ } else {
+ if (nullOrBlank(type)) {
+ ctx.addIssue(new ValidationIssue(
+ ctx.currentKey(),
+ "Key store type must not be blank"
+ ));
+ }
+
+ if (nullOrBlank(path)) {
+ ctx.addIssue(new ValidationIssue(
+ ctx.currentKey(),
+ "Key store path must not be blank"
+ ));
+ }
+ }
+ }
+}
diff --git
a/modules/network/src/test/java/org/apache/ignite/internal/network/configuration/KeyStoreConfigurationValidatorImplTest.java
b/modules/network/src/test/java/org/apache/ignite/internal/network/configuration/KeyStoreConfigurationValidatorImplTest.java
new file mode 100644
index 0000000000..9399660210
--- /dev/null
+++
b/modules/network/src/test/java/org/apache/ignite/internal/network/configuration/KeyStoreConfigurationValidatorImplTest.java
@@ -0,0 +1,77 @@
+/*
+ * 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.ignite.internal.network.configuration;
+
+import static
org.apache.ignite.internal.configuration.validation.TestValidationUtil.mockValidationContext;
+import static
org.apache.ignite.internal.configuration.validation.TestValidationUtil.validate;
+import static org.mockito.Mockito.mock;
+
+import org.apache.ignite.configuration.validation.ValidationContext;
+import org.junit.jupiter.api.Test;
+
+class KeyStoreConfigurationValidatorImplTest {
+
+ @Test
+ public void nullPath() {
+ ValidationContext<KeyStoreView> ctx = mockValidationContext(
+ null,
+ new StubKeyStoreView("PKCS12", null, "changeIt")
+ );
+ validate(KeyStoreConfigurationValidatorImpl.INSTANCE,
mock(KeyStoreConfigurationValidator.class), ctx,
+ "Key store path must not be blank");
+ }
+
+ @Test
+ public void emptyPath() {
+ ValidationContext<KeyStoreView> ctx = mockValidationContext(
+ null,
+ new StubKeyStoreView("PKCS12", "", "changeIt")
+ );
+ validate(KeyStoreConfigurationValidatorImpl.INSTANCE,
mock(KeyStoreConfigurationValidator.class), ctx,
+ "Key store path must not be blank");
+ }
+
+ @Test
+ public void nullType() {
+ ValidationContext<KeyStoreView> ctx = mockValidationContext(
+ null,
+ new StubKeyStoreView(null, "/path/to/keystore.p12", null)
+ );
+ validate(KeyStoreConfigurationValidatorImpl.INSTANCE,
mock(KeyStoreConfigurationValidator.class), ctx,
+ "Key store type must not be blank");
+ }
+
+ @Test
+ public void emptyType() {
+ ValidationContext<KeyStoreView> ctx = mockValidationContext(
+ null,
+ new StubKeyStoreView("", "/path/to/keystore.p12", null)
+ );
+ validate(KeyStoreConfigurationValidatorImpl.INSTANCE,
mock(KeyStoreConfigurationValidator.class), ctx,
+ "Key store type must not be blank");
+ }
+
+ @Test
+ public void validConfig() {
+ ValidationContext<KeyStoreView> ctx = mockValidationContext(
+ null,
+ new StubKeyStoreView("PKCS12", "/path/to/keystore.p12", null)
+ );
+ validate(KeyStoreConfigurationValidatorImpl.INSTANCE,
mock(KeyStoreConfigurationValidator.class), ctx, null);
+ }
+}
diff --git
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationModule.java
b/modules/network/src/test/java/org/apache/ignite/internal/network/configuration/StubKeyStoreView.java
similarity index 53%
copy from
modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationModule.java
copy to
modules/network/src/test/java/org/apache/ignite/internal/network/configuration/StubKeyStoreView.java
index bcee31330e..6afa2128c2 100644
---
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationModule.java
+++
b/modules/network/src/test/java/org/apache/ignite/internal/network/configuration/StubKeyStoreView.java
@@ -15,27 +15,36 @@
* limitations under the License.
*/
-package org.apache.ignite.internal.rest.configuration;
+package org.apache.ignite.internal.network.configuration;
-import com.google.auto.service.AutoService;
-import java.util.Collection;
-import java.util.Collections;
-import org.apache.ignite.configuration.RootKey;
-import org.apache.ignite.configuration.annotation.ConfigurationType;
-import org.apache.ignite.internal.configuration.ConfigurationModule;
+/** Stub of {@link KeyStoreView} for tests. */
+public class StubKeyStoreView implements KeyStoreView {
+
+ private String type;
+
+ private String path;
+
+ private String password;
+
+ /** Constructor. */
+ public StubKeyStoreView(String type, String path, String password) {
+ this.type = type;
+ this.path = path;
+ this.password = password;
+ }
+
+ @Override
+ public String type() {
+ return type;
+ }
-/**
- * {@link ConfigurationModule} for node-local configuration provided by
ignite-rest.
- */
-@AutoService(ConfigurationModule.class)
-public class RestConfigurationModule implements ConfigurationModule {
@Override
- public ConfigurationType type() {
- return ConfigurationType.LOCAL;
+ public String path() {
+ return path;
}
@Override
- public Collection<RootKey<?, ?>> rootKeys() {
- return Collections.singleton(RestConfiguration.KEY);
+ public String password() {
+ return password;
}
}
diff --git
a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/cluster/NodeMetadataDto.java
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/cluster/NodeMetadataDto.java
index 3d64d162e2..c777febb3b 100644
---
a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/cluster/NodeMetadataDto.java
+++
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/cluster/NodeMetadataDto.java
@@ -29,18 +29,24 @@ import org.apache.ignite.network.NodeMetadata;
@Schema(name = "NodeMetadata")
public class NodeMetadataDto {
private final String restHost;
- private final int restPort;
+ private final int httpPort;
+ private final int httpsPort;
/**
* Constructor.
*
* @param restHost REST host of a node.
- * @param restPort REST port of a node.
+ * @param httpPort HTTP port of a node.
+ * @param httpsPort HTTPS port of a node.
*/
@JsonCreator
- public NodeMetadataDto(@JsonProperty("restHost") String restHost,
@JsonProperty("restPort") int restPort) {
+ public NodeMetadataDto(
+ @JsonProperty("restHost") String restHost,
+ @JsonProperty("httpPort") int httpPort,
+ @JsonProperty("httpsPort") int httpsPort) {
this.restHost = restHost;
- this.restPort = restPort;
+ this.httpPort = httpPort;
+ this.httpsPort = httpsPort;
}
/**
@@ -54,12 +60,22 @@ public class NodeMetadataDto {
}
/**
- * Returns this node's REST port.
+ * Returns this node's HTTP port.
*
- * @return REST port.
+ * @return HTTP port.
*/
- @JsonGetter("restPort")
- public int restPort() {
- return restPort;
+ @JsonGetter("httpPort")
+ public int httpPort() {
+ return httpPort;
+ }
+
+ /**
+ * Returns this node's HTTPS port.
+ *
+ * @return HTTPS port.
+ */
+ @JsonGetter("httpsPort")
+ public int httpsPort() {
+ return httpsPort;
}
}
diff --git a/modules/rest/build.gradle b/modules/rest/build.gradle
index 3dc864680b..3e1dea30d4 100644
--- a/modules/rest/build.gradle
+++ b/modules/rest/build.gradle
@@ -30,6 +30,7 @@ dependencies {
implementation project(':ignite-configuration')
implementation project(':ignite-rest-api')
implementation project(':ignite-core')
+ implementation project(':ignite-network')
implementation libs.jetbrains.annotations
implementation libs.micronaut.inject
implementation libs.micronaut.http.server.netty
diff --git a/modules/rest/openapi/openapi.yaml
b/modules/rest/openapi/openapi.yaml
index 9f37d67525..fcb5edfec0 100644
--- a/modules/rest/openapi/openapi.yaml
+++ b/modules/rest/openapi/openapi.yaml
@@ -516,13 +516,17 @@ components:
format: int32
NodeMetadata:
required:
+ - httpPort
+ - httpsPort
- restHost
- - restPort
type: object
properties:
restHost:
type: string
- restPort:
+ httpPort:
+ type: integer
+ format: int32
+ httpsPort:
type: integer
format: int32
NodeState:
diff --git
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/RestComponent.java
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/RestComponent.java
index b0878b207b..d5fc16c084 100644
---
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/RestComponent.java
+++
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/RestComponent.java
@@ -43,6 +43,7 @@ import
org.apache.ignite.internal.rest.api.configuration.NodeConfigurationApi;
import org.apache.ignite.internal.rest.api.metric.NodeMetricApi;
import org.apache.ignite.internal.rest.api.node.NodeManagementApi;
import org.apache.ignite.internal.rest.configuration.RestConfiguration;
+import org.apache.ignite.internal.rest.configuration.RestSslView;
import org.apache.ignite.internal.rest.configuration.RestView;
import org.apache.ignite.lang.IgniteInternalException;
import org.jetbrains.annotations.Nullable;
@@ -67,8 +68,9 @@ import org.jetbrains.annotations.Nullable;
TopologyApi.class
})
public class RestComponent implements IgniteComponent {
- /** Default port. */
- public static final int DFLT_PORT = 10300;
+
+ /** Unavailable port. */
+ private static final int UNAVAILABLE_PORT = -1;
/** Server host. */
private static final String LOCALHOST = "localhost";
@@ -85,7 +87,10 @@ public class RestComponent implements IgniteComponent {
private volatile ApplicationContext context;
/** Server port. */
- private int port;
+ private int httpPort = UNAVAILABLE_PORT;
+
+ /** Server SSL port. */
+ private int httpsPort = UNAVAILABLE_PORT;
/**
* Creates a new instance of REST module.
@@ -99,37 +104,77 @@ public class RestComponent implements IgniteComponent {
@Override
public void start() {
RestView restConfigurationView = restConfiguration.value();
+ RestSslView sslConfigurationView = restConfigurationView.ssl();
- int desiredPort = restConfigurationView.port();
+ boolean sslEnabled = sslConfigurationView.enabled();
+ boolean dualProtocol = restConfiguration.dualProtocol().value();
+ int desiredHttpPort = restConfigurationView.port();
int portRange = restConfigurationView.portRange();
+ int desiredHttpsPort = sslConfigurationView.port();
+ int httpsPortRange = sslConfigurationView.portRange();
+ int httpPortCandidate = desiredHttpPort;
+ int httpsPortCandidate = desiredHttpsPort;
- for (int portCandidate = desiredPort; portCandidate <= desiredPort +
portRange; portCandidate++) {
- try {
- port = portCandidate;
- context = buildMicronautContext(portCandidate)
- .deduceEnvironment(false)
- .environments(BARE_METAL)
- .start();
- LOG.info("REST protocol started successfully");
+ while (httpPortCandidate <= desiredHttpPort + portRange
+ && httpsPortCandidate <= desiredHttpsPort + httpsPortRange) {
+ if (startServer(httpPortCandidate, httpsPortCandidate)) {
return;
- } catch (ApplicationStartupException e) {
- BindException bindException = findBindException(e);
- if (bindException != null) {
- LOG.debug("Got exception during node start, going to try
again [port={}]", portCandidate);
- continue;
- }
- throw new RuntimeException(e);
+ }
+
+ LOG.debug("Got exception during node start, going to try again
[httpPort={}, httpsPort={}]",
+ httpPortCandidate,
+ httpsPortCandidate);
+
+ if (sslEnabled && dualProtocol) {
+ httpPortCandidate++;
+ httpsPortCandidate++;
+ } else if (sslEnabled) {
+ httpsPortCandidate++;
+ } else {
+ httpPortCandidate++;
}
}
- LOG.debug("Unable to start REST endpoint. All ports are in use
[ports=[{}, {}]]", desiredPort, (desiredPort + portRange));
+ LOG.debug("Unable to start REST endpoint."
+ + " Couldn't find available port for HTTP or HTTPS"
+ + " [HTTP ports=[{}, {}]],"
+ + " [HTTPS ports=[{}, {}]]",
+ desiredHttpPort, (desiredHttpPort + portRange),
+ desiredHttpsPort, (desiredHttpsPort + httpsPortRange));
- String msg = "Cannot start REST endpoint. " + "All ports in range [" +
desiredPort + ", " + (desiredPort + portRange)
- + "] are in use.";
+ String msg = "Cannot start REST endpoint."
+ + " Couldn't find available port for HTTP or HTTPS"
+ + " [HTTP ports=[" + desiredHttpPort + ", " + desiredHttpPort
+ portRange + "]],"
+ + " [HTTPS ports=[" + desiredHttpsPort + ", " +
desiredHttpsPort + httpsPortRange + "]]";
throw new RuntimeException(msg);
}
+ /** Starts Micronaut application using the provided ports.
+ *
+ * @param httpPortCandidate HTTP port candidate.
+ * @param httpsPortCandidate HTTPS port candidate.
+ * @return {@code True} if server was started successfully, {@code False}
if couldn't bind one of the ports.
+ */
+ private boolean startServer(int httpPortCandidate, int httpsPortCandidate)
{
+ try {
+ httpPort = httpPortCandidate;
+ httpsPort = httpsPortCandidate;
+ context = buildMicronautContext(httpPortCandidate,
httpsPortCandidate)
+ .deduceEnvironment(false)
+ .environments(BARE_METAL)
+ .start();
+ LOG.info("REST protocol started successfully");
+ return true;
+ } catch (ApplicationStartupException e) {
+ BindException bindException = findBindException(e);
+ if (bindException != null) {
+ return false;
+ }
+ throw new RuntimeException(e);
+ }
+ }
+
@Nullable
private BindException findBindException(ApplicationStartupException e) {
var throwable = e.getCause();
@@ -142,11 +187,11 @@ public class RestComponent implements IgniteComponent {
return null;
}
- private Micronaut buildMicronautContext(int portCandidate) {
+ private Micronaut buildMicronautContext(int portCandidate, int
sslPortCandidate) {
Micronaut micronaut = Micronaut.build("");
setFactories(micronaut);
return micronaut
- .properties(Map.of("micronaut.server.port", portCandidate))
+ .properties(properties(portCandidate, sslPortCandidate))
.banner(false)
.mapError(ServerStartupException.class,
this::mapServerStartupException)
.mapError(ApplicationStartupException.class, ex -> -1);
@@ -166,6 +211,28 @@ public class RestComponent implements IgniteComponent {
}
}
+ private Map<String, Object> properties(int port, int sslPort) {
+ boolean dualProtocol = restConfiguration.dualProtocol().value();
+ boolean sslEnabled = restConfiguration.ssl().enabled().value();
+ String keyStoreType =
restConfiguration.ssl().keyStore().type().value();
+ String keyStorePath =
restConfiguration.ssl().keyStore().path().value();
+ String keyStorePassword =
restConfiguration.ssl().keyStore().password().value();
+
+ if (sslEnabled) {
+ return Map.of(
+ "micronaut.server.port", port, // Micronaut is not going
to handle requests on that port, but it's required
+ "micronaut.server.dual-protocol", dualProtocol,
+ "micronaut.server.ssl.port", sslPort,
+ "micronaut.server.ssl.enabled", sslEnabled,
+ "micronaut.server.ssl.key-store.path", "file:" +
keyStorePath,
+ "micronaut.server.ssl.key-store.password",
keyStorePassword,
+ "micronaut.server.ssl.key-store.type", keyStoreType
+ );
+ } else {
+ return Map.of("micronaut.server.port", port);
+ }
+ }
+
/** {@inheritDoc} */
@Override
public synchronized void stop() throws Exception {
@@ -179,19 +246,37 @@ public class RestComponent implements IgniteComponent {
/**
* Returns server port.
*
+ * @return server port or -1 if HTTP is unavailable.
* @throws IgniteInternalException if the component has not been started
yet.
*/
- public int port() {
- if (context == null) {
- throw new IgniteInternalException("RestComponent has not been
started");
+ public int httpPort() {
+ RestView restView = restConfiguration.value();
+ if (!restView.ssl().enabled() || restView.dualProtocol()) {
+ return httpPort;
+ } else {
+ return UNAVAILABLE_PORT;
}
+ }
- return port;
+ /**
+ * Returns server SSL port.
+ *
+ * @return server SSL port or -1 if HTTPS is unavailable.
+ * @throws IgniteInternalException if the component has not been started
yet.
+ */
+ public int httpsPort() {
+ RestView restView = restConfiguration.value();
+ if (restView.ssl().enabled()) {
+ return httpsPort;
+ } else {
+ return UNAVAILABLE_PORT;
+ }
}
/**
* Returns server host.
*
+ * @return host.
* @throws IgniteInternalException if the component has not been started
yet.
*/
public String host() {
diff --git
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationModule.java
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationModule.java
index bcee31330e..9558de9f33 100644
---
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationModule.java
+++
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationModule.java
@@ -20,9 +20,12 @@ package org.apache.ignite.internal.rest.configuration;
import com.google.auto.service.AutoService;
import java.util.Collection;
import java.util.Collections;
+import java.util.Set;
import org.apache.ignite.configuration.RootKey;
import org.apache.ignite.configuration.annotation.ConfigurationType;
+import org.apache.ignite.configuration.validation.Validator;
import org.apache.ignite.internal.configuration.ConfigurationModule;
+import
org.apache.ignite.internal.network.configuration.KeyStoreConfigurationValidatorImpl;
/**
* {@link ConfigurationModule} for node-local configuration provided by
ignite-rest.
@@ -38,4 +41,9 @@ public class RestConfigurationModule implements
ConfigurationModule {
public Collection<RootKey<?, ?>> rootKeys() {
return Collections.singleton(RestConfiguration.KEY);
}
+
+ @Override
+ public Set<Validator<?, ?>> validators() {
+ return Set.of(KeyStoreConfigurationValidatorImpl.INSTANCE);
+ }
}
diff --git
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationSchema.java
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationSchema.java
index cabd1c5a6b..6d06d7cf39 100644
---
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationSchema.java
+++
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationSchema.java
@@ -17,6 +17,7 @@
package org.apache.ignite.internal.rest.configuration;
+import org.apache.ignite.configuration.annotation.ConfigValue;
import org.apache.ignite.configuration.annotation.ConfigurationRoot;
import org.apache.ignite.configuration.annotation.ConfigurationType;
import org.apache.ignite.configuration.annotation.Value;
@@ -37,4 +38,16 @@ public class RestConfigurationSchema {
@Range(min = 0)
@Value(hasDefault = true)
public final int portRange = 100;
+
+ /** The dual protocol (http/https) configuration. */
+ @Value(hasDefault = true)
+ public final boolean dualProtocol = false;
+
+ /** HTTP to HTTPS redirection. */
+ @Value(hasDefault = true)
+ public final boolean httpToHttpsRedirection = false;
+
+ /** SSL configuration. */
+ @ConfigValue
+ public RestSslConfigurationSchema ssl;
}
diff --git
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationSchema.java
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestSslConfigurationSchema.java
similarity index 61%
copy from
modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationSchema.java
copy to
modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestSslConfigurationSchema.java
index cabd1c5a6b..00da64cad5 100644
---
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationSchema.java
+++
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestSslConfigurationSchema.java
@@ -17,24 +17,33 @@
package org.apache.ignite.internal.rest.configuration;
-import org.apache.ignite.configuration.annotation.ConfigurationRoot;
-import org.apache.ignite.configuration.annotation.ConfigurationType;
+import org.apache.ignite.configuration.annotation.Config;
+import org.apache.ignite.configuration.annotation.ConfigValue;
import org.apache.ignite.configuration.annotation.Value;
import org.apache.ignite.configuration.validation.Range;
+import
org.apache.ignite.internal.network.configuration.KeyStoreConfigurationSchema;
+import
org.apache.ignite.internal.network.configuration.KeyStoreConfigurationValidator;
-/**
- * Configuration schema for REST endpoint subtree.
- */
-@SuppressWarnings("PMD.UnusedPrivateField")
-@ConfigurationRoot(rootName = "rest", type = ConfigurationType.LOCAL)
-public class RestConfigurationSchema {
- /** TCP port. */
+/** REST SSL configuration. */
+@Config
+public class RestSslConfigurationSchema {
+
+ /** Whether SSL is enabled. */
+ @Value(hasDefault = true)
+ public final boolean enabled = false;
+
+ /** SSL port. */
@Range(min = 1024, max = 0xFFFF)
@Value(hasDefault = true)
- public final int port = 10300;
+ public final int port = 10400;
- /** TCP port range. */
+ /** SSL port range. */
@Range(min = 0)
@Value(hasDefault = true)
public final int portRange = 100;
+
+ /** SSL keystore. */
+ @KeyStoreConfigurationValidator
+ @ConfigValue
+ public KeyStoreConfigurationSchema keyStore;
}
diff --git
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/ItPortRangeTest.java
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/ItPortRangeTest.java
new file mode 100644
index 0000000000..3098c447a4
--- /dev/null
+++
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/ItPortRangeTest.java
@@ -0,0 +1,130 @@
+/*
+ * 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.ignite.internal.rest;
+
+import static
org.apache.ignite.internal.testframework.IgniteTestUtils.testNodeName;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.nio.file.Path;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.cert.CertificateException;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManagerFactory;
+import org.apache.ignite.internal.rest.ssl.ItRestSslTest;
+import org.apache.ignite.internal.rest.ssl.RestNode;
+import org.apache.ignite.internal.testframework.WorkDirectory;
+import org.apache.ignite.internal.testframework.WorkDirectoryExtension;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.TestInfo;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/** Tests for the REST port range configuration. */
+@ExtendWith(WorkDirectoryExtension.class)
+public class ItPortRangeTest {
+
+ /** Trust store path. */
+ private static final String trustStorePath = "ssl/truststore.jks";
+
+ /** Trust store password. */
+ private static final String trustStorePassword = "changeIt";
+
+ /** Path to the working directory. */
+ @WorkDirectory
+ private Path workDir;
+
+ /** SSL HTTP client that is expected to be defined in subclasses. */
+ private HttpClient sslClient;
+
+
+ @BeforeEach
+ void beforeEach(TestInfo testInfo)
+ throws CertificateException, KeyStoreException, IOException,
NoSuchAlgorithmException, KeyManagementException {
+ sslClient = HttpClient.newBuilder()
+ .sslContext(sslContext())
+ .build();
+ }
+
+ private static Stream<Arguments> sslConfigurationProperties() {
+ return Stream.of(
+ Arguments.of(false, false),
+ Arguments.of(false, true),
+ Arguments.of(true, false),
+ Arguments.of(true, true)
+ );
+ }
+
+ @ParameterizedTest
+ @DisplayName("Port range works in all configurations")
+ @MethodSource("sslConfigurationProperties")
+ void portRange(boolean sslEnabled, boolean dualProtocol, TestInfo
testInfo) throws IOException, InterruptedException {
+ List<RestNode> nodes = IntStream.range(0, 3)
+ .mapToObj(id -> new RestNode(
+ workDir,
+ testNodeName(testInfo, id),
+ 3522 + id,
+ 10300,
+ 10400,
+ true,
+ true
+ ))
+ .collect(Collectors.toList());
+ try {
+ nodes.forEach(RestNode::start);
+ // When GET /management/v1/configuration/node
+ String httpAddress = sslEnabled ? nodes.get(0).httpsAddress() :
nodes.get(0).httpAddress();
+ URI uri = URI.create(httpAddress +
"/management/v1/configuration/node");
+ HttpRequest request = HttpRequest.newBuilder(uri).build();
+
+ // Then response code is 200
+ HttpResponse<String> response = sslClient.send(request,
BodyHandlers.ofString());
+ assertEquals(200, response.statusCode());
+ } finally {
+ nodes.forEach(RestNode::stop);
+ }
+ }
+
+ private static SSLContext sslContext()
+ throws CertificateException, KeyStoreException, IOException,
NoSuchAlgorithmException, KeyManagementException {
+ String path =
ItRestSslTest.class.getClassLoader().getResource(trustStorePath).getPath();
+ TrustManagerFactory trustManagerFactory =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ KeyStore keyStore = KeyStore.getInstance(new File(path),
trustStorePassword.toCharArray());
+ trustManagerFactory.init(keyStore);
+ SSLContext sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(null, trustManagerFactory.getTrustManagers(), new
SecureRandom());
+ return sslContext;
+ }
+}
diff --git
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/ssl/ItRestSslTest.java
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/ssl/ItRestSslTest.java
new file mode 100644
index 0000000000..70b5f14722
--- /dev/null
+++
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/ssl/ItRestSslTest.java
@@ -0,0 +1,175 @@
+/*
+ * 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.ignite.internal.rest.ssl;
+
+import static
org.apache.ignite.internal.testframework.IgniteTestUtils.testNodeName;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.nio.file.Path;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.cert.CertificateException;
+import java.util.stream.Stream;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManagerFactory;
+import org.apache.ignite.internal.testframework.WorkDirectory;
+import org.apache.ignite.internal.testframework.WorkDirectoryExtension;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+/** Tests for the REST SSL configuration. */
+@ExtendWith(WorkDirectoryExtension.class)
+public class ItRestSslTest {
+
+ /** HTTP port of the test node. */
+ private static final int httpPort = 10300;
+
+ /** HTTPS port of the test node. */
+ private static final int httpsPort = 10400;
+
+ /** Trust store path. */
+ private static final String trustStorePath = "ssl/truststore.jks";
+
+ /** Trust store password. */
+ private static final String trustStorePassword = "changeIt";
+
+ /** Path to the working directory. */
+ @WorkDirectory
+ private static Path workDir;
+
+ /** HTTP client that is expected to be defined in subclasses. */
+ private static HttpClient client;
+
+ /** SSL HTTP client that is expected to be defined in subclasses. */
+ private static HttpClient sslClient;
+
+ private static RestNode httpNode;
+
+ private static RestNode httpsNode;
+
+ private static RestNode dualProtocolNode;
+
+ @BeforeAll
+ static void beforeAll(TestInfo testInfo) throws Exception {
+
+ client = HttpClient.newBuilder()
+ .build();
+
+ sslClient = HttpClient.newBuilder()
+ .sslContext(sslContext())
+ .build();
+
+ httpNode = new RestNode(workDir, testNodeName(testInfo, 3344), 3344,
10300, 10400, false, false);
+ httpsNode = new RestNode(workDir, testNodeName(testInfo, 3345), 3345,
10301, 10401, true, false);
+ dualProtocolNode = new RestNode(workDir, testNodeName(testInfo, 3346),
3346, 10302, 10402, true, true);
+ Stream.of(httpNode, httpsNode, dualProtocolNode)
+ .forEach(RestNode::start);
+ }
+
+ @Test
+ void httpsProtocol() throws IOException, InterruptedException {
+ // When GET /management/v1/configuration/node
+ URI uri = URI.create(httpsNode.httpsAddress() +
"/management/v1/configuration/node");
+ HttpRequest request = HttpRequest.newBuilder(uri).build();
+
+ // Then response code is 200
+ HttpResponse<String> response = sslClient.send(request,
BodyHandlers.ofString());
+ assertEquals(200, response.statusCode());
+ }
+
+ @Test
+ void httpProtocol(TestInfo testInfo) throws IOException,
InterruptedException {
+ // When GET /management/v1/configuration/node
+ URI uri = URI.create(httpNode.httpAddress() +
"/management/v1/configuration/node");
+ HttpRequest request = HttpRequest.newBuilder(uri).build();
+
+ // Then response code is 200
+ HttpResponse<String> response = client.send(request,
BodyHandlers.ofString());
+ assertEquals(200, response.statusCode());
+ }
+
+ @Test
+ void dualProtocol() throws IOException, InterruptedException {
+ // When GET /management/v1/configuration/node
+ URI httpUri = URI.create(dualProtocolNode.httpAddress() +
"/management/v1/configuration/node");
+ HttpRequest httpRequest = HttpRequest.newBuilder(httpUri).build();
+
+ URI httpsUri = URI.create(dualProtocolNode.httpsAddress() +
"/management/v1/configuration/node");
+ HttpRequest httpsRequest = HttpRequest.newBuilder(httpsUri).build();
+
+ // Then HTTP response code is 200
+ HttpResponse<String> httpResponse = client.send(httpRequest,
BodyHandlers.ofString());
+ assertEquals(200, httpResponse.statusCode());
+
+ // And HTTPS response code is 200
+ httpResponse = sslClient.send(httpsRequest, BodyHandlers.ofString());
+ assertEquals(200, httpResponse.statusCode());
+ }
+
+ @Test
+ void httpsProtocolNotSslClient() {
+ // When GET /management/v1/configuration/node
+ URI uri = URI.create(httpsNode.httpsAddress() +
"/management/v1/configuration/node");
+ HttpRequest request = HttpRequest.newBuilder(uri).build();
+
+ // Then IOException
+ assertThrows(IOException.class, () -> client.send(request,
BodyHandlers.ofString()));
+
+ }
+
+ @Test
+ void httpProtocolNotSslClient() throws IOException, InterruptedException {
+ // When GET /management/v1/configuration/node
+ URI uri = URI.create(httpsNode.httpAddress() +
"/management/v1/configuration/node");
+ HttpRequest request = HttpRequest.newBuilder(uri).build();
+
+ // Expect IOException
+ assertThrows(IOException.class, () -> client.send(request,
BodyHandlers.ofString()));
+ }
+
+ @AfterAll
+ static void afterAll() {
+ Stream.of(httpNode, httpsNode, dualProtocolNode)
+ .forEach(RestNode::stop);
+ }
+
+ private static SSLContext sslContext()
+ throws CertificateException, KeyStoreException, IOException,
NoSuchAlgorithmException, KeyManagementException {
+ String path =
ItRestSslTest.class.getClassLoader().getResource(trustStorePath).getPath();
+ TrustManagerFactory trustManagerFactory =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ KeyStore keyStore = KeyStore.getInstance(new File(path),
trustStorePassword.toCharArray());
+ trustManagerFactory.init(keyStore);
+ SSLContext sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(null, trustManagerFactory.getTrustManagers(), new
SecureRandom());
+ return sslContext;
+ }
+}
diff --git
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/ssl/RestNode.java
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/ssl/RestNode.java
new file mode 100644
index 0000000000..f554526994
--- /dev/null
+++
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/ssl/RestNode.java
@@ -0,0 +1,99 @@
+/*
+ * 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.ignite.internal.rest.ssl;
+
+import java.nio.file.Path;
+import org.apache.ignite.IgnitionManager;
+
+/** Presentation of Ignite node for tests. */
+public class RestNode {
+
+ /** Key store path. */
+ private static final String keyStorePath = "ssl/keystore.p12";
+
+ /** Key store password. */
+ private static final String keyStorePassword = "changeIt";
+
+ private final Path workDir;
+ private final String name;
+ private final int networkPort;
+ private final int httpPort;
+ private final int httpsPort;
+ private final boolean sslEnabled;
+ private final boolean dualProtocol;
+
+ /** Constructor. */
+ public RestNode(
+ Path workDir,
+ String name,
+ int networkPort,
+ int httpPort,
+ int httpsPort,
+ boolean sslEnabled,
+ boolean dualProtocol
+ ) {
+ this.workDir = workDir;
+ this.name = name;
+ this.networkPort = networkPort;
+ this.httpPort = httpPort;
+ this.httpsPort = httpsPort;
+ this.sslEnabled = sslEnabled;
+ this.dualProtocol = dualProtocol;
+ }
+
+ public RestNode start() {
+ IgnitionManager.start(name, bootstrapCfg(), workDir.resolve(name));
+ return this;
+ }
+
+ public void stop() {
+ IgnitionManager.stop(name);
+ }
+
+ public String httpAddress() {
+ return "http://localhost:" + httpPort;
+ }
+
+ public String httpsAddress() {
+ return "https://localhost:" + httpsPort;
+ }
+
+ private String bootstrapCfg() {
+ String keyStoreAbsolutPath =
ItRestSslTest.class.getClassLoader().getResource(keyStorePath).getPath();
+ return "{\n"
+ + " network: {\n"
+ + " port: " + networkPort + ",\n"
+ + " nodeFinder: {\n"
+ + " netClusterNodes: [ \"localhost:3344\",
\"localhost:3345\", \"localhost:3346\" ]\n"
+ + " }\n"
+ + " },\n"
+ + " rest: {\n"
+ + " port: " + httpPort + ",\n"
+ + " dualProtocol: " + dualProtocol + ",\n"
+ + " ssl: {\n"
+ + " enabled: " + sslEnabled + ",\n"
+ + " port: " + httpsPort + ",\n"
+ + " keyStore: {\n"
+ + " path: " + keyStoreAbsolutPath + ",\n"
+ + " password: " + keyStorePassword + "\n"
+ + " }\n"
+ + " }\n"
+ + " }"
+ + "}";
+ }
+}
diff --git a/modules/runner/src/integrationTest/resources/ssl/keystore.p12
b/modules/runner/src/integrationTest/resources/ssl/keystore.p12
new file mode 100644
index 0000000000..1677d2a561
Binary files /dev/null and
b/modules/runner/src/integrationTest/resources/ssl/keystore.p12 differ
diff --git a/modules/runner/src/integrationTest/resources/ssl/truststore.jks
b/modules/runner/src/integrationTest/resources/ssl/truststore.jks
new file mode 100644
index 0000000000..a20d9db42d
Binary files /dev/null and
b/modules/runner/src/integrationTest/resources/ssl/truststore.jks differ
diff --git
a/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java
b/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java
index 008116e5e0..f95df43dd0 100644
---
a/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java
+++
b/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java
@@ -568,9 +568,9 @@ public class IgniteImpl implements Ignite {
cmgMgr
);
- clusterSvc.updateMetadata(new NodeMetadata(restComponent.host(),
restComponent.port()));
+ clusterSvc.updateMetadata(new NodeMetadata(restComponent.host(),
restComponent.httpPort(), restComponent.httpsPort()));
- restAddressReporter.writeReport(restAddress());
+ restAddressReporter.writeReport(restHttpAddress(),
restHttpsAddress());
LOG.info("Components started, joining the cluster");
@@ -755,13 +755,38 @@ public class IgniteImpl implements Ignite {
}
/**
- * Returns the local address of REST endpoints.
+ * Returns the local HTTP address of REST endpoints.
*
+ * @return address or null if HTTP is not enabled.
* @throws IgniteInternalException if the REST module is not started.
*/
// TODO: should be encapsulated in local properties, see
https://issues.apache.org/jira/browse/IGNITE-15131
- public NetworkAddress restAddress() {
- return new NetworkAddress(restComponent.host(), restComponent.port());
+ @Nullable
+ public NetworkAddress restHttpAddress() {
+ String host = restComponent.host();
+ int port = restComponent.httpPort();
+ if (port != -1) {
+ return new NetworkAddress(host, port);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the local HTTPS address of REST endpoints.
+ *
+ * @return address or null if HTTPS is not enabled.
+ * @throws IgniteInternalException if the REST module is not started.
+ */
+ // TODO: should be encapsulated in local properties, see
https://issues.apache.org/jira/browse/IGNITE-15131
+ public NetworkAddress restHttpsAddress() {
+ String host = restComponent.host();
+ int port = restComponent.httpsPort();
+ if (port != -1) {
+ return new NetworkAddress(host, port);
+ } else {
+ return null;
+ }
}
/**
diff --git
a/modules/runner/src/main/java/org/apache/ignite/internal/component/RestAddressReporter.java
b/modules/runner/src/main/java/org/apache/ignite/internal/component/RestAddressReporter.java
index 9d389a230d..51e1b2677c 100644
---
a/modules/runner/src/main/java/org/apache/ignite/internal/component/RestAddressReporter.java
+++
b/modules/runner/src/main/java/org/apache/ignite/internal/component/RestAddressReporter.java
@@ -20,9 +20,13 @@ package org.apache.ignite.internal.component;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
import org.apache.ignite.lang.ErrorGroups.Common;
import org.apache.ignite.lang.IgniteException;
import org.apache.ignite.network.NetworkAddress;
+import org.jetbrains.annotations.Nullable;
/**
* Can write network address to file that could be used by other systems to
know on what port Ignite 3 REST server is started.
@@ -39,15 +43,30 @@ public class RestAddressReporter {
}
/** Write network address to file. */
- public void writeReport(NetworkAddress networkAddress) {
+ public void writeReport(@Nullable NetworkAddress httpAddress, @Nullable
NetworkAddress httpsAddress) {
try {
- Files.writeString(workDir.resolve(REPORT_FILE_NAME), "http://" +
networkAddress.host() + ":" + networkAddress.port());
+ Files.writeString(workDir.resolve(REPORT_FILE_NAME),
report(httpAddress, httpsAddress));
} catch (IOException e) {
String message = "Unexpected error when trying to write REST
server network address to file";
throw new IgniteException(Common.UNEXPECTED_ERR, message, e);
}
}
+ private String report(@Nullable NetworkAddress httpAddress, @Nullable
NetworkAddress httpsAddress) {
+ return Stream.of(report("http", httpAddress), report("https",
httpsAddress))
+ .filter(Objects::nonNull)
+ .collect(Collectors.joining(", "));
+ }
+
+ @Nullable
+ private String report(String protocol, @Nullable NetworkAddress
httpAddress) {
+ if (httpAddress == null) {
+ return null;
+ } else {
+ return protocol + "://" + httpAddress.host() + ":" +
httpAddress.port();
+ }
+ }
+
/** Remove report file. The method is expected to be called on node stop.
*/
public void removeReport() {
try {
diff --git
a/modules/runner/src/test/java/org/apache/ignite/internal/component/RestAddressReporterTest.java
b/modules/runner/src/test/java/org/apache/ignite/internal/component/RestAddressReporterTest.java
index 3463100dde..174cd12a16 100644
---
a/modules/runner/src/test/java/org/apache/ignite/internal/component/RestAddressReporterTest.java
+++
b/modules/runner/src/test/java/org/apache/ignite/internal/component/RestAddressReporterTest.java
@@ -39,25 +39,53 @@ class RestAddressReporterTest {
private static final String REST_ADDRESS_FILENAME = "rest-address";
@Test
- @DisplayName("REST server network address is reported to file")
- void networkAddressReported(@TempDir Path tmpDir) throws IOException {
+ @DisplayName("REST server network addresses is reported to file")
+ void httpAndHttpsAddressesReported(@TempDir Path tmpDir) throws
IOException {
// Given
RestAddressReporter reporter = new RestAddressReporter(tmpDir);
// When
- reporter.writeReport(new NetworkAddress("localhost", 9999));
+ reporter.writeReport(new NetworkAddress("localhost", 9999), new
NetworkAddress("localhost", 8443));
+
+ // Then there is a report
+ String restAddress =
Files.readString(tmpDir.resolve(REST_ADDRESS_FILENAME));
+ assertThat(restAddress, equalTo("http://localhost:9999,
https://localhost:8443"));
+ }
+
+ @Test
+ @DisplayName("REST server HTTP address is reported to file")
+ void httpAddressReported(@TempDir Path tmpDir) throws IOException {
+ // Given
+ RestAddressReporter reporter = new RestAddressReporter(tmpDir);
+
+ // When
+ reporter.writeReport(new NetworkAddress("localhost", 9999), null);
// Then there is a report
String restAddress =
Files.readString(tmpDir.resolve(REST_ADDRESS_FILENAME));
assertThat(restAddress, equalTo("http://localhost:9999"));
}
+ @Test
+ @DisplayName("REST server HTTPS address is reported to file")
+ void httpsAddressReported(@TempDir Path tmpDir) throws IOException {
+ // Given
+ RestAddressReporter reporter = new RestAddressReporter(tmpDir);
+
+ // When
+ reporter.writeReport(null, new NetworkAddress("localhost", 8443));
+
+ // Then there is a report
+ String restAddress =
Files.readString(tmpDir.resolve(REST_ADDRESS_FILENAME));
+ assertThat(restAddress, equalTo("https://localhost:8443"));
+ }
+
@Test
@DisplayName("File with network address is removed")
void reportDeleted(@TempDir Path tmpDir) throws IOException {
// Given reported address
RestAddressReporter reporter = new RestAddressReporter(tmpDir);
- reporter.writeReport(new NetworkAddress("localhost", 9999));
+ reporter.writeReport(new NetworkAddress("localhost", 9999), new
NetworkAddress("localhost", 8443));
// And file exists
assertThat(Files.exists(tmpDir.resolve(REST_ADDRESS_FILENAME)),
is(true));
@@ -94,10 +122,11 @@ class RestAddressReporterTest {
);
// When try to write it again but with another port
- new RestAddressReporter(tmpDir).writeReport(new
NetworkAddress("localhost", 4444));
+ RestAddressReporter reporter = new RestAddressReporter(tmpDir);
+ reporter.writeReport(new NetworkAddress("localhost", 4444), new
NetworkAddress("localhost", 8443));
// Then file rewritten
String restAddress =
Files.readString(tmpDir.resolve(REST_ADDRESS_FILENAME));
- assertThat(restAddress, equalTo("http://localhost:4444"));
+ assertThat(restAddress, equalTo("http://localhost:4444,
https://localhost:8443"));
}
}
diff --git a/packaging/zip/ignite3db b/packaging/zip/ignite3db
index 898af35d36..12b004aa7f 100755
--- a/packaging/zip/ignite3db
+++ b/packaging/zip/ignite3db
@@ -41,7 +41,7 @@ start() {
while [ ! -f "$rest_address_file" ]; do sleep 0.5; done
rest_address=$(cat "$rest_address_file")
- echo "Node named ${NODE_NAME} started successfully. REST address is
$rest_address"
+ echo "Node named ${NODE_NAME} started successfully. REST addresses are
[$rest_address]"
}
stop() {