diff --git a/config/src/main/java/com/quorum/tessera/config/ServerConfig.java b/config/src/main/java/com/quorum/tessera/config/ServerConfig.java index feeb7a50c4..70da063c5c 100644 --- a/config/src/main/java/com/quorum/tessera/config/ServerConfig.java +++ b/config/src/main/java/com/quorum/tessera/config/ServerConfig.java @@ -1,5 +1,6 @@ package com.quorum.tessera.config; +import com.quorum.tessera.config.constraints.ValidServerAddress; import com.quorum.tessera.config.constraints.ValidSsl; import javax.validation.Valid; @@ -14,7 +15,6 @@ @XmlAccessorType(XmlAccessType.FIELD) public class ServerConfig extends ConfigItem { - //TODO validate that the server socket type and the communication type match the AppType @NotNull @XmlElement(required = true) private AppType app; @@ -23,7 +23,6 @@ public class ServerConfig extends ConfigItem { @XmlElement(required = true) private boolean enabled; - @XmlElement private CommunicationType communicationType; @@ -36,9 +35,15 @@ public class ServerConfig extends ConfigItem { @XmlElement private InfluxConfig influxConfig; + @ValidServerAddress( + message = "Binding Address is invalid", + isBindingAddress = true, + supportedSchemes = {"http","https"} + ) @XmlElement private String bindingAddress; + @ValidServerAddress(message = "Server Address is invalid") @NotNull @XmlElement private String serverAddress; diff --git a/config/src/main/java/com/quorum/tessera/config/constraints/ServerAddressValidator.java b/config/src/main/java/com/quorum/tessera/config/constraints/ServerAddressValidator.java new file mode 100644 index 0000000000..5a0b6f4323 --- /dev/null +++ b/config/src/main/java/com/quorum/tessera/config/constraints/ServerAddressValidator.java @@ -0,0 +1,67 @@ +package com.quorum.tessera.config.constraints; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +public class ServerAddressValidator implements ConstraintValidator { + + private static final Logger LOGGER = LoggerFactory.getLogger(ServerAddressValidator.class); + + private boolean bindingAddress; + + private List supportedSchemes; + + @Override + public void initialize(ValidServerAddress a) { + this.supportedSchemes = Arrays.asList(a.supportedSchemes()); + this.bindingAddress = a.isBindingAddress(); + } + + @Override + public boolean isValid(String v, ConstraintValidatorContext cvc) { + + if(Objects.isNull(v)) { + return true; + } + + final URI uri; + try{ + uri = new URI(v); + } catch (URISyntaxException ex) { + LOGGER.debug(v,ex); + return false; + } + + String scheme = uri.getScheme(); + + if(!supportedSchemes.contains(scheme)) { + return false; + } + + if(scheme.startsWith("http")) { + if(uri.getPort() == -1) { + return false; + } + } + + if(bindingAddress) { + return true; + } + + if(Objects.equals("unix",scheme)) { + return true; + } + + return !Objects.equals("0.0.0.0", uri.getHost()); + + } + +} diff --git a/config/src/main/java/com/quorum/tessera/config/constraints/ValidServerAddress.java b/config/src/main/java/com/quorum/tessera/config/constraints/ValidServerAddress.java new file mode 100644 index 0000000000..d58278ee14 --- /dev/null +++ b/config/src/main/java/com/quorum/tessera/config/constraints/ValidServerAddress.java @@ -0,0 +1,31 @@ +package com.quorum.tessera.config.constraints; + +import java.lang.annotation.Documented; +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE_PARAMETER; +import static java.lang.annotation.ElementType.TYPE_USE; +import java.lang.annotation.Retention; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import java.lang.annotation.Target; +import javax.validation.Constraint; +import javax.validation.Payload; + +@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, TYPE_PARAMETER, TYPE_USE}) +@Retention(RUNTIME) +@Constraint(validatedBy = ServerAddressValidator.class) +@Documented +public @interface ValidServerAddress { + + boolean isBindingAddress() default false; + + String[] supportedSchemes() default {"unix","http","https"}; + + String message() default "{ValidServerAddress.message}"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/config/src/test/java/com/quorum/tessera/config/ValidationTest.java b/config/src/test/java/com/quorum/tessera/config/ValidationTest.java index fa40efd1a8..99dd69745e 100644 --- a/config/src/test/java/com/quorum/tessera/config/ValidationTest.java +++ b/config/src/test/java/com/quorum/tessera/config/ValidationTest.java @@ -127,7 +127,7 @@ public void invalidAlwaysSendTo() { List alwaysSendTo = singletonList("BOGUS"); - Config config = new Config(null, null, null, null, alwaysSendTo, null, false,false); + Config config = new Config(null, null, null, null, alwaysSendTo, null, false, false); Set> violations = validator.validateProperty(config, "alwaysSendTo"); @@ -145,7 +145,7 @@ public void validAlwaysSendTo() { List alwaysSendTo = singletonList(value); - Config config = new Config(null, null, null, null, alwaysSendTo, null, false,false); + Config config = new Config(null, null, null, null, alwaysSendTo, null, false, false); Set> violations = validator.validateProperty(config, "alwaysSendTo"); @@ -174,7 +174,7 @@ public void keypairPathsValidation() { assertThat(violation2.getMessageTemplate()).isEqualTo("File does not exist"); final List paths = Arrays.asList( - violation1.getPropertyPath().toString(), violation2.getPropertyPath().toString() + violation1.getPropertyPath().toString(), violation2.getPropertyPath().toString() ); assertThat(paths).containsExactlyInAnyOrder("keyData[0].publicKeyPath", "keyData[0].privateKeyPath"); } @@ -213,8 +213,8 @@ public void azureKeyPairIdsDisallowedCharactersCreateViolation() { assertThat(violations).hasSize(2); assertThat(violations).extracting("messageTemplate") - .containsExactly("Azure Key Vault key IDs can only contain alphanumeric characters and dashes (-)", - "Azure Key Vault key IDs can only contain alphanumeric characters and dashes (-)"); + .containsExactly("Azure Key Vault key IDs can only contain alphanumeric characters and dashes (-)", + "Azure Key Vault key IDs can only contain alphanumeric characters and dashes (-)"); } @Test @@ -235,7 +235,7 @@ public void azureKeyPairKeyVersionLongerThan32CharsCreatesViolation() { assertThat(violations).hasSize(2); assertThat(violations).extracting("messageTemplate") - .containsExactly("length must be 32 characters", "length must be 32 characters"); + .containsExactly("length must be 32 characters", "length must be 32 characters"); } @Test @@ -247,7 +247,7 @@ public void azureKeyPairKeyVersionShorterThan32CharsCreatesViolation() { assertThat(violations).hasSize(2); assertThat(violations).extracting("messageTemplate") - .containsExactly("length must be 32 characters", "length must be 32 characters"); + .containsExactly("length must be 32 characters", "length must be 32 characters"); } @Test @@ -418,4 +418,28 @@ public void hashicorpVaultKeyPairProvidedButKeyVaultConfigHasNullUrlCreatesNotNu assertThat(violation.getMessageTemplate()).isEqualTo("{javax.validation.constraints.NotNull.message}"); assertThat(violation.getPropertyPath().toString()).isEqualTo("hashicorpKeyVaultConfig.url"); } + + @Test + public void serverAddressValidations() { + + String[] invalidAddresses = {"/foo/bar","foo@bar.com,:/fff.ll","file:/tmp/valid.somename"}; + + ServerConfig config = new ServerConfig(); + for (String sample : invalidAddresses) { + config.setServerAddress(sample); + Set> validresult = validator.validateProperty(config, "serverAddress"); + assertThat(validresult).hasSize(1); + } + + + + String[] validSamples = {"unix:/foo/bar.ipc","http://localhost:8080","https://somestrangedomain.com:8080"}; + for (String sample : validSamples) { + config.setServerAddress(sample); + Set> validresult = validator.validateProperty(config, "serverAddress"); + assertThat(validresult).isEmpty(); + } + + } + } diff --git a/config/src/test/java/com/quorum/tessera/config/constraints/ServerAddressValidatorTest.java b/config/src/test/java/com/quorum/tessera/config/constraints/ServerAddressValidatorTest.java new file mode 100644 index 0000000000..02668e59a7 --- /dev/null +++ b/config/src/test/java/com/quorum/tessera/config/constraints/ServerAddressValidatorTest.java @@ -0,0 +1,118 @@ +package com.quorum.tessera.config.constraints; + +import javax.validation.ConstraintValidatorContext; +import static org.assertj.core.api.Assertions.assertThat; +import org.junit.Test; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ServerAddressValidatorTest { + + @Test + public void valid() { + + ServerAddressValidator validator + = new ServerAddressValidator(); + + ValidServerAddress validServerAddress = mock(ValidServerAddress.class); + when(validServerAddress.supportedSchemes()).thenReturn(new String[]{"http"}); + + validator.initialize(validServerAddress); + + ConstraintValidatorContext context = mock(ConstraintValidatorContext.class); + + assertThat(validator.isValid("https://www.ilovesparraws.com:80/somepatth", context)).isFalse(); + assertThat(validator.isValid("http://www.ilovesparraws.com:80/somepatth", context)).isTrue(); + } + + @Test + public void dontAllowZeroIpWhenNotBindingAddress() { + + ServerAddressValidator validator + = new ServerAddressValidator(); + + ValidServerAddress validServerAddress = mock(ValidServerAddress.class); + + when(validServerAddress.isBindingAddress()).thenReturn(false); + when(validServerAddress.supportedSchemes()).thenReturn(new String[]{"http"}); + + validator.initialize(validServerAddress); + + ConstraintValidatorContext context = mock(ConstraintValidatorContext.class); + + assertThat(validator.isValid("http://0.0.0.0:80", context)).isFalse(); + + } + + @Test + public void dontAllowZeroIpWhenBindingAddress() { + + ServerAddressValidator validator + = new ServerAddressValidator(); + + ValidServerAddress validServerAddress = mock(ValidServerAddress.class); + + when(validServerAddress.isBindingAddress()).thenReturn(true); + when(validServerAddress.supportedSchemes()).thenReturn(new String[]{"http"}); + + validator.initialize(validServerAddress); + + ConstraintValidatorContext context = mock(ConstraintValidatorContext.class); + + assertThat(validator.isValid("http://0.0.0.0:80", context)).isTrue(); + + } + + @Test + public void nullValueIsIgnored() { + + ServerAddressValidator validator = new ServerAddressValidator(); + + ValidServerAddress validServerAddress = mock(ValidServerAddress.class); + + when(validServerAddress.isBindingAddress()).thenReturn(true); + when(validServerAddress.supportedSchemes()).thenReturn(new String[]{"http"}); + + validator.initialize(validServerAddress); + + ConstraintValidatorContext context = mock(ConstraintValidatorContext.class); + + assertThat(validator.isValid(null, context)).isTrue(); + + } + + @Test + public void unixSchemStypeIntCheckedForZeroIp() { + + ServerAddressValidator validator = new ServerAddressValidator(); + + ValidServerAddress validServerAddress = mock(ValidServerAddress.class); + + when(validServerAddress.isBindingAddress()).thenReturn(false); + when(validServerAddress.supportedSchemes()).thenReturn(new String[]{"unix"}); + + validator.initialize(validServerAddress); + + ConstraintValidatorContext context = mock(ConstraintValidatorContext.class); + + assertThat(validator.isValid("unix:/bogus", context)).isTrue(); + + } + + @Test + public void httpPortIsRequired() { + + ServerAddressValidator validator + = new ServerAddressValidator(); + + ValidServerAddress validServerAddress = mock(ValidServerAddress.class); + when(validServerAddress.supportedSchemes()).thenReturn(new String[]{"http"}); + + validator.initialize(validServerAddress); + + ConstraintValidatorContext context = mock(ConstraintValidatorContext.class); + + assertThat(validator.isValid("http://www.ilovesparraws.com", context)).isFalse(); + } + +}