This is an automated email from the ASF dual-hosted git repository. rcordier pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 3a7233cef82ff46706f78334cf227880ea160c39 Author: Rene Cordier <rcord...@linagora.com> AuthorDate: Wed Mar 11 11:44:59 2020 +0700 JAMES-3078 Reactor-Netty JMAPServer implementation --- core/src/main/java/org/apache/james/util/Port.java | 16 ++++ .../test/java/org/apache/james/util/PortTest.java | 9 ++ server/protocols/jmap/pom.xml | 72 ++++++++++++++ .../java/org/apache/james/jmap/HttpConstants.java | 46 +-------- .../org/apache/james/jmap/JMAPConfiguration.java | 104 +++++++++++++++++++++ .../java/org/apache/james/jmap/JMAPRoutes.java | 60 ++++++++++++ .../java/org/apache/james/jmap/JMAPServer.java | 70 ++++++++++++++ .../apache/james/jmap/JMAPConfigurationTest.java | 87 +++++++++++++++++ .../java/org/apache/james/jmap/JMAPServerTest.java | 87 +++++++++++++++++ 9 files changed, 510 insertions(+), 41 deletions(-) diff --git a/core/src/main/java/org/apache/james/util/Port.java b/core/src/main/java/org/apache/james/util/Port.java index 7f84202..90191d4 100644 --- a/core/src/main/java/org/apache/james/util/Port.java +++ b/core/src/main/java/org/apache/james/util/Port.java @@ -19,6 +19,7 @@ package org.apache.james.util; +import java.util.Objects; import java.util.concurrent.ThreadLocalRandom; import com.google.common.base.Preconditions; @@ -59,4 +60,19 @@ public class Port { public int getValue() { return value; } + + @Override + public final boolean equals(Object o) { + if (o instanceof Port) { + Port indexName = (Port) o; + + return Objects.equals(this.value, indexName.value); + } + return false; + } + + @Override + public final int hashCode() { + return Objects.hash(value); + } } diff --git a/core/src/test/java/org/apache/james/util/PortTest.java b/core/src/test/java/org/apache/james/util/PortTest.java index 793c162..b46acea 100644 --- a/core/src/test/java/org/apache/james/util/PortTest.java +++ b/core/src/test/java/org/apache/james/util/PortTest.java @@ -24,7 +24,16 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import org.junit.jupiter.api.Test; +import nl.jqno.equalsverifier.EqualsVerifier; + class PortTest { + + @Test + void portShouldRespectBeanContract() { + EqualsVerifier.forClass(Port.class) + .verify(); + } + @Test void assertValidShouldThrowOnNegativePort() { assertThatThrownBy(() -> Port.assertValid(-1)) diff --git a/server/protocols/jmap/pom.xml b/server/protocols/jmap/pom.xml new file mode 100644 index 0000000..6116486 --- /dev/null +++ b/server/protocols/jmap/pom.xml @@ -0,0 +1,72 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * 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. * + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <parent> + <artifactId>james-server</artifactId> + <groupId>org.apache.james</groupId> + <version>3.5.0-SNAPSHOT</version> + <relativePath>../../pom.xml</relativePath> + </parent> + <modelVersion>4.0.0</modelVersion> + + <artifactId>james-server-jmap</artifactId> + + <dependencies> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>james-core</artifactId> + </dependency> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>james-server-lifecycle-api</artifactId> + </dependency> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>james-server-util</artifactId> + </dependency> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>testing-base</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.google.guava</groupId> + <artifactId>guava</artifactId> + </dependency> + <dependency> + <groupId>io.projectreactor.netty</groupId> + <artifactId>reactor-netty</artifactId> + </dependency> + <dependency> + <groupId>io.rest-assured</groupId> + <artifactId>rest-assured</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>jcl-over-slf4j</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + +</project> \ No newline at end of file diff --git a/core/src/main/java/org/apache/james/util/Port.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/HttpConstants.java similarity index 50% copy from core/src/main/java/org/apache/james/util/Port.java copy to server/protocols/jmap/src/main/java/org/apache/james/jmap/HttpConstants.java index 7f84202..ce50efa 100644 --- a/core/src/main/java/org/apache/james/util/Port.java +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/HttpConstants.java @@ -17,46 +17,10 @@ * under the License. * ****************************************************************/ -package org.apache.james.util; +package org.apache.james.jmap; -import java.util.concurrent.ThreadLocalRandom; - -import com.google.common.base.Preconditions; -import com.google.common.collect.Range; - -public class Port { - public static final int MAX_PORT_VALUE = 65535; - public static final int PRIVILEGED_PORT_BOUND = 1024; - private static final Range<Integer> VALID_PORT_RANGE = Range.closed(1, MAX_PORT_VALUE); - - public static Port of(int portNumber) { - return new Port(portNumber); - } - - public static int generateValidUnprivilegedPort() { - return ThreadLocalRandom.current().nextInt(Port.MAX_PORT_VALUE - PRIVILEGED_PORT_BOUND) + PRIVILEGED_PORT_BOUND; - } - - public static void assertValid(int port) { - Preconditions.checkArgument(isValid(port), "Port should be between 1 and 65535"); - } - - public static boolean isValid(int port) { - return VALID_PORT_RANGE.contains(port); - } - - private final int value; - - public Port(int value) { - validate(value); - this.value = value; - } - - protected void validate(int port) { - assertValid(port); - } - - public int getValue() { - return value; - } +public interface HttpConstants { + String JSON_CONTENT_TYPE = "application/json"; + String JSON_CONTENT_TYPE_UTF8 = "application/json; charset=UTF-8"; + String TEXT_PLAIN_CONTENT_TYPE = "text/plain"; } diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPConfiguration.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPConfiguration.java new file mode 100644 index 0000000..a54f012 --- /dev/null +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPConfiguration.java @@ -0,0 +1,104 @@ +/**************************************************************** + * 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.james.jmap; + +import java.util.Optional; + +import org.apache.james.util.Port; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; + +public class JMAPConfiguration { + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private Optional<Boolean> enabled = Optional.empty(); + private Optional<Boolean> wiretap = Optional.empty(); + private Optional<Port> port = Optional.empty(); + + private Builder() { + + } + + public Builder enabled(boolean enabled) { + this.enabled = Optional.of(enabled); + return this; + } + + public Builder wiretap() { + return wiretap(true); + } + + public Builder wiretap(boolean enabled) { + this.wiretap = Optional.of(enabled); + return this; + } + + public Builder enable() { + return enabled(true); + } + + public Builder disable() { + return enabled(false); + } + + public Builder port(Port port) { + this.port = Optional.of(port); + return this; + } + + public Builder randomPort() { + this.port = Optional.empty(); + return this; + } + + public JMAPConfiguration build() { + Preconditions.checkState(enabled.isPresent(), "You should specify if JMAP server should be started"); + return new JMAPConfiguration(enabled.get(), wiretap.orElse(false), port); + } + + } + + private final boolean enabled; + private final boolean wiretap; + private final Optional<Port> port; + + @VisibleForTesting + JMAPConfiguration(boolean enabled, boolean wiretap, Optional<Port> port) { + this.enabled = enabled; + this.wiretap = wiretap; + this.port = port; + } + + public boolean wiretapEnabled() { + return wiretap; + } + + public boolean isEnabled() { + return enabled; + } + + public Optional<Port> getPort() { + return port; + } +} diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPRoutes.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPRoutes.java new file mode 100644 index 0000000..4c76901 --- /dev/null +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPRoutes.java @@ -0,0 +1,60 @@ +/**************************************************************** + * 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.james.jmap; + +import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; +import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR; +import static io.netty.handler.codec.http.HttpResponseStatus.UNAUTHORIZED; + +import java.util.function.BiFunction; + +import org.reactivestreams.Publisher; +import org.slf4j.Logger; + +import reactor.core.publisher.Mono; +import reactor.netty.http.server.HttpServerRequest; +import reactor.netty.http.server.HttpServerResponse; +import reactor.netty.http.server.HttpServerRoutes; + +public interface JMAPRoutes { + HttpServerRoutes define(HttpServerRoutes builder); + + BiFunction<HttpServerRequest, HttpServerResponse, Publisher<Void>> CORS_CONTROL = (req, res) -> res.header("Access-Control-Allow-Origin", "*") + .header("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT") + .header("Access-Control-Allow-Headers", "Content-Type, Authorization, Accept") + .send(); + + Logger logger(); + + default Mono<Void> handleInternalError(HttpServerResponse response, Throwable e) { + logger().error("Internal error", e); + return response.status(INTERNAL_SERVER_ERROR).send(); + } + + default Mono<Void> handleBadRequest(HttpServerResponse response, Exception e) { + logger().warn("Invalid request received.", e); + return response.status(BAD_REQUEST).send(); + } + + default Mono<Void> handleAuthenticationFailure(HttpServerResponse response, Exception e) { + logger().warn("Unauthorized", e); + return response.status(UNAUTHORIZED).send(); + } +} diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPServer.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPServer.java new file mode 100644 index 0000000..7b52ce1 --- /dev/null +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPServer.java @@ -0,0 +1,70 @@ +/**************************************************************** + * 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.james.jmap; + +import java.util.Optional; +import java.util.Set; + +import javax.annotation.PreDestroy; +import javax.inject.Inject; + +import org.apache.james.lifecycle.api.Startable; +import org.apache.james.util.Port; + +import reactor.netty.DisposableServer; +import reactor.netty.http.server.HttpServer; + +public class JMAPServer implements Startable { + private static final int RANDOM_PORT = 0; + + private final JMAPConfiguration configuration; + private final Set<JMAPRoutes> jmapRoutes; + private Optional<DisposableServer> server; + + @Inject + public JMAPServer(JMAPConfiguration configuration, Set<JMAPRoutes> jmapRoutes) { + this.configuration = configuration; + this.jmapRoutes = jmapRoutes; + this.server = Optional.empty(); + } + + public Port getPort() { + return server.map(DisposableServer::port) + .map(Port::of) + .orElseThrow(() -> new IllegalStateException("port is not available because server is not started or disabled")); + } + + public void start() { + if (configuration.isEnabled()) { + server = Optional.of(HttpServer.create() + .port(configuration.getPort() + .map(Port::getValue) + .orElse(RANDOM_PORT)) + .route(routes -> jmapRoutes.forEach(jmapRoute -> jmapRoute.define(routes))) + .wiretap(configuration.wiretapEnabled()) + .bindNow()); + } + } + + @PreDestroy + public void stop() { + server.ifPresent(DisposableServer::disposeNow); + } +} diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/JMAPConfigurationTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/JMAPConfigurationTest.java new file mode 100644 index 0000000..a9d9c64 --- /dev/null +++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/JMAPConfigurationTest.java @@ -0,0 +1,87 @@ +/**************************************************************** + * 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.james.jmap; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Java6Assertions.assertThat; + +import java.util.Optional; + +import org.apache.james.util.Port; +import org.junit.jupiter.api.Test; + +class JMAPConfigurationTest { + + public static final boolean ENABLED = true; + public static final boolean DISABLED = false; + + @Test + void buildShouldThrowWhenEnableIsMissing() { + assertThatThrownBy(() -> JMAPConfiguration.builder().build()) + .isInstanceOf(IllegalStateException.class) + .hasMessage("You should specify if JMAP server should be started"); + } + + @Test + void buildShouldWorkWhenRandomPort() { + JMAPConfiguration expectedJMAPConfiguration = new JMAPConfiguration(ENABLED, false, Optional.empty()); + + JMAPConfiguration jmapConfiguration = JMAPConfiguration.builder() + .enable() + .randomPort() + .build(); + assertThat(jmapConfiguration).isEqualToComparingFieldByField(expectedJMAPConfiguration); + } + + @Test + public void buildShouldWorkWhenFixedPort() { + JMAPConfiguration expectedJMAPConfiguration = new JMAPConfiguration(ENABLED, false, Optional.of(Port.of(80))); + + JMAPConfiguration jmapConfiguration = JMAPConfiguration.builder() + .enable() + .port(Port.of(80)) + .build(); + + assertThat(jmapConfiguration).isEqualToComparingFieldByField(expectedJMAPConfiguration); + } + + @Test + public void buildShouldWorkWhenWiretap() { + JMAPConfiguration expectedJMAPConfiguration = new JMAPConfiguration(ENABLED, true, Optional.empty()); + + JMAPConfiguration jmapConfiguration = JMAPConfiguration.builder() + .enable() + .wiretap() + .randomPort() + .build(); + + assertThat(jmapConfiguration).isEqualToComparingFieldByField(expectedJMAPConfiguration); + } + + @Test + public void buildShouldWorkWhenDisabled() { + JMAPConfiguration expectedJMAPConfiguration = new JMAPConfiguration(DISABLED, false, Optional.empty()); + + JMAPConfiguration jmapConfiguration = JMAPConfiguration.builder() + .disable() + .build(); + assertThat(jmapConfiguration).isEqualToComparingFieldByField(expectedJMAPConfiguration); + } +} diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/JMAPServerTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/JMAPServerTest.java new file mode 100644 index 0000000..1c36384 --- /dev/null +++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/JMAPServerTest.java @@ -0,0 +1,87 @@ +/**************************************************************** + * 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.james.jmap; + +import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +import com.google.common.collect.ImmutableSet; + +class JMAPServerTest { + private static final JMAPConfiguration DISABLED_CONFIGURATION = JMAPConfiguration.builder().disable().build(); + private static final JMAPConfiguration TEST_CONFIGURATION = JMAPConfiguration.builder() + .enable() + .randomPort() + .build(); + private static final ImmutableSet<JMAPRoutes> NO_ROUTES = ImmutableSet.of(); + + @Test + void serverShouldAnswerWhenStarted() { + JMAPServer jmapServer = new JMAPServer(TEST_CONFIGURATION, NO_ROUTES); + jmapServer.start(); + + try { + given() + .port(jmapServer.getPort().getValue()) + .basePath("http://localhost") + .when() + .get() + .then() + .statusCode(404); + } finally { + jmapServer.stop(); + } + } + + @Test + void startShouldNotThrowWhenConfigurationDisabled() { + JMAPServer jmapServer = new JMAPServer(DISABLED_CONFIGURATION, NO_ROUTES); + + assertThatCode(jmapServer::start).doesNotThrowAnyException(); + } + + @Test + void stopShouldNotThrowWhenConfigurationDisabled() { + JMAPServer jmapServer = new JMAPServer(DISABLED_CONFIGURATION, NO_ROUTES); + jmapServer.start(); + + assertThatCode(jmapServer::stop).doesNotThrowAnyException(); + } + + @Test + void getPortShouldThrowWhenServerIsNotStarted() { + JMAPServer jmapServer = new JMAPServer(TEST_CONFIGURATION, NO_ROUTES); + + assertThatThrownBy(jmapServer::getPort) + .isInstanceOf(IllegalStateException.class); + } + + @Test + void getPortShouldThrowWhenDisabledConfiguration() { + JMAPServer jmapServer = new JMAPServer(DISABLED_CONFIGURATION, NO_ROUTES); + jmapServer.start(); + + assertThatThrownBy(jmapServer::getPort) + .isInstanceOf(IllegalStateException.class); + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: server-dev-unsubscr...@james.apache.org For additional commands, e-mail: server-dev-h...@james.apache.org