/*
* #%L
* Wisdom-Framework
* %%
* Copyright (C) 2013 - 2015 Wisdom Framework
* %%
* Licensed 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.
* #L%
*/
package org.wisdom.framework.vertx;
import com.google.common.base.Preconditions;
import io.vertx.core.*;
import io.vertx.core.http.ClientAuth;
import io.vertx.core.http.HttpServer;
import io.vertx.core.http.HttpServerOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.wisdom.api.configuration.ApplicationConfiguration;
import org.wisdom.api.configuration.Configuration;
import org.wisdom.api.http.Result;
import org.wisdom.api.http.Results;
import org.wisdom.framework.vertx.ssl.SSLServerContext;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.regex.Pattern;
/**
* Class representing the server configuration and configuring the server.
*/
public class Server {
/**
* Random used to generate random port.
* No need for a secure random here, as this random is just to find a free port.
* The field is marked as volatile to avoid the half-initialization if two threads access the class at the same
* time.
*/
private static volatile Random random = new Random(); //NOSONAR we don't need a secure random here.
/**
* The name of the server.
*/
private final String name;
/**
* The interface / host on which the server is bound. By default all interfaces are bound.
*/
private final String host;
/**
* The logger.
*/
private final Logger logger; // NOSONAR the name is configured per instance.
/**
* The vert.x instance.
*/
private final Vertx vertx;
/**
* The service accessor.
*/
private final ServiceAccessor accessor;
/**
* The application configuration.
*/
private final ApplicationConfiguration configuration;
/**
* The listened port, updated once the server is bound (that's why the field is not final).
*/
private int port;
/**
* whether or not SSL is enabled.
*/
private final boolean ssl;
/**
* whether or not the mutual authentication is enabled.
*/
private final boolean authentication;
/**
* The list of accepted patterns.
*/
private List<Pattern> allow;
/**
* The list of denied patterns
*/
private List<Pattern> deny;
/**
* The url on which the request is redirected when the request is denied. By default, if not set a `FORBIDDEN`
* result is returned.
*/
private String onDenied;
/**
* The HTTP server.
*/
private HttpServer http;
private Context context;
/**
* Creates the default HTTP server (listening on port 9000 / `http.port`), no SSL, no mutual authentication,
* accept all requests.
*
* @param accessor the service accessor
* @param vertx the vertx singleton
* @return the configured server (not bound, not started)
*/
public static Server defaultHttp(ServiceAccessor accessor, Vertx vertx) {
return new Server(
accessor,
vertx,
"default-http",
accessor.getConfiguration().getIntegerWithDefault("http.port", 9000),
false, false,
null,
Collections.<String>emptyList(), Collections.<String>emptyList(), null);
}
/**
* Creates the default HTTPS server (listening on port 9001 / `https.port`), SSL enabled, no mutual authentication,
* accept all requests.
*
* @param accessor the service accessor
* @param vertx the vertx singleton
* @return the configured server (not bound, not started)
*/
public static Server defaultHttps(ServiceAccessor accessor, Vertx vertx) {
return new Server(
accessor,
vertx,
"default-https",
accessor.getConfiguration().getIntegerWithDefault("https.port", 9001),
true, false,
null,
Collections.<String>emptyList(), Collections.<String>emptyList(), null);
}
/**
* Creates a new server from the given configuration object.
*
* @param accessor the service accessor
* @param vertx the vertx singleton
* @param name the server name
* @param configuration the configuration
* @return the configured server (not bound, not started)
*/
public static Server from(ServiceAccessor accessor,
Vertx vertx,
String name,
Configuration configuration) {
return new Server(
accessor,
vertx,
name,
configuration.getIntegerOrDie("port"),
configuration.getBooleanWithDefault("ssl", false),
configuration.getBooleanWithDefault("authentication", false),
configuration.get("host"),
configuration.getList("allow"),
configuration.getList("deny"),
configuration.get("onDenied")
);
}
/**
* Creates a new server.
*
* @param accessor the service accessor
* @param vertx the vertx singleton
* @param name the server name
* @param port the port
* @param ssl whether or not SSL is enabled
* @param host the listened interface
* @param allow the set of path with wildcards accepted by the server
* @param deny the set of path with wildcards rejected by the server
* @param authentication whether or not mutual authentication is enabled
* @param onDenied the redirection URL if a request is denied by the server
*/
public Server(ServiceAccessor accessor,
Vertx vertx,
String name, int port,
boolean ssl, boolean authentication,
String host,
List<String> allow, List<String> deny, String onDenied) {
Preconditions.checkNotNull(accessor);
Preconditions.checkNotNull(vertx);
Preconditions.checkNotNull(name);
this.accessor = accessor;
this.configuration = accessor.getConfiguration();
this.vertx = vertx;
this.name = name;
if (host == null) {
this.host = "0.0.0.0";
} else {
this.host = host;
}
this.port = port;
this.ssl = ssl;
this.authentication = authentication;
List<Pattern> allowedPatterns = new ArrayList<>();
List<Pattern> deniedPatterns = new ArrayList<>();
for (String a : allow) {
allowedPatterns.add(Pattern.compile(a.trim().replace(".", "\\.").replace("*", ".*")));
}
for (String a : deny) {
deniedPatterns.add(Pattern.compile(a.trim().replace(".", "\\.").replace("*", ".*")));
}
this.allow = allowedPatterns;
this.deny = deniedPatterns;
this.onDenied = onDenied;
this.logger = LoggerFactory.getLogger("server-" + name);
}
/**
* Starts the server. The server is going to try to listen on the given host / port. Startup is asynchronous. You
* can pull {@link #port()} to know when the server has successfully be bound (in case of a random port).
*/
public void bind(Handler<AsyncResult<Void>> completion) {
logger.info("Starting server {}", name);
context = vertx.getOrCreateContext();
bind(port, completion);
}
private void bind(int p, Handler<AsyncResult<Void>> completion) {
// Get port number.
final int thePort = pickAPort(port);
HttpServerOptions options = new HttpServerOptions();
if (ssl) {
options.setSsl(true);
options.setTrustStoreOptions(SSLServerContext.getTrustStoreOption(accessor));
options.setKeyStoreOptions(SSLServerContext.getKeyStoreOption(accessor));
if (authentication) {
options.setClientAuth(ClientAuth.REQUIRED);
}
}
if (hasCompressionEnabled()) {
options.setCompressionSupported(true);
}
if (configuration.getIntegerWithDefault("vertx.acceptBacklog", -1) != -1) {
options.setAcceptBacklog(configuration.getInteger("vertx.acceptBacklog"));
}
if (configuration.getIntegerWithDefault("vertx.maxWebSocketFrameSize", -1) != -1) {
options.setMaxWebsocketFrameSize(configuration.getInteger("vertx.maxWebSocketFrameSize"));
}
if (configuration.getStringArray("wisdom.websocket.subprotocols").length > 0) {
options.setWebsocketSubProtocols(configuration.get("wisdom.websocket.subprotocols"));
}
if (configuration.getStringArray("vertx.websocket-subprotocols").length > 0) {
options.setWebsocketSubProtocols(configuration.get("vertx.websocket-subprotocols"));
}
if (configuration.getIntegerWithDefault("vertx.receiveBufferSize", -1) != -1) {
options.setReceiveBufferSize(configuration.getInteger("vertx.receiveBufferSize"));
}
if (configuration.getIntegerWithDefault("vertx.sendBufferSize", -1) != -1) {
options.setSendBufferSize(configuration.getInteger("vertx.sendBufferSize"));
}
http = vertx.createHttpServer(options)
.requestHandler(new HttpHandler(vertx, accessor, this))
.websocketHandler(new WebSocketHandler(accessor, this));
http.listen(thePort, host, event -> {
if (event.succeeded()) {
logger.info("Wisdom is going to serve HTTP requests on port {}.", thePort);
port = thePort;
completion.handle(Future.succeededFuture());
} else if (port == 0) {
logger.debug("Cannot bind on port {} (port already used probably)", thePort, event.cause());
bind(0, completion);
} else {
logger.error("Cannot bind on port {} (port already used probably)", thePort, event.cause());
completion.handle(Future.failedFuture("Cannot bind on port " + thePort));
}
});
}
/**
* Checks whether the given path is accepted or rejected by the current server.
*
* @param path the path
* @return {@code true} if the path is accepted, {@code false} otherwise.
*/
public boolean accept(String path) {
if (allow.isEmpty() && deny.isEmpty()) {
return true;
}
// Check if the path is denied
for (Pattern p : deny) {
if (p.matcher(path).matches()) {
return false;
}
}
// Check if the path is accepted
for (Pattern p : allow) {
if (p.matcher(path).matches()) {
return true;
}
}
// Denied by default.
return !deny.isEmpty();
}
public Result getOnDeniedResult() {
if (onDenied == null) {
return Results.forbidden();
} else {
return Results.redirect(onDenied);
}
}
private int pickAPort(int port) {
if (port == 0) {
port = 9000 + random.nextInt(10000);
logger.debug("Random port lookup - Trying with {}", port);
}
return port;
}
/**
* Gets the server's name.
*
* @return the name
*/
public String name() {
return name;
}
/**
* Stops / Closes the server.
*/
public void close(Handler<AsyncResult<Void>> completion) {
if (context == null) {
context = vertx.getOrCreateContext();
}
context.runOnContext(v -> {
if (http != null) {
http.close(event -> {
logger.info("The server '{}' has been stopped (bound port: {})", name, port);
completion.handle(Future.<Void>succeededFuture());
});
}
});
}
/**
* Gets whether or not SSL is enabled on the current server.
*
* @return {@code true} if SSL is enabled, {@code false} otherwise
*/
public boolean ssl() {
return ssl;
}
/**
* Gets the port listen by the server.
*
* @return the listened port, 0 is not bound yet.
*/
public int port() {
return port;
}
/**
* Gets the host on which the server is bound.
*
* @return the host, {@code 0.0.0.0} means all network interfaces
*/
public String host() {
return host;
}
/**
* @return whether or not the compression is enabled.
*/
public boolean hasCompressionEnabled() {
return configuration.getBooleanWithDefault("vertx.compression", true);
}
/**
* @return the threshold below which the content should not be encoded. By default
* it's {@link ApplicationConfiguration#DEFAULT_ENCODING_MIN_SIZE} bytes.
*/
public long getEncodingMinBound() {
return configuration.getBytes(ApplicationConfiguration.ENCODING_MIN_SIZE,
ApplicationConfiguration.DEFAULT_ENCODING_MIN_SIZE);
}
/**
* @return the threshold above which the content should not be encoded. By default
* it's {@link ApplicationConfiguration#DEFAULT_ENCODING_MAX_SIZE} bytes.
*/
public long getEncodingMaxBound() {
return configuration.getBytes(ApplicationConfiguration.ENCODING_MAX_SIZE,
ApplicationConfiguration.DEFAULT_ENCODING_MAX_SIZE);
}
}