
com.arcadedb.server.http.HttpServer Maven / Gradle / Ivy
The newest version!
/*
* Copyright © 2021-present Arcade Data Ltd ([email protected])
*
* 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.
*
* SPDX-FileCopyrightText: 2021-present Arcade Data Ltd ([email protected])
* SPDX-License-Identifier: Apache-2.0
*/
package com.arcadedb.server.http;
import com.arcadedb.ContextConfiguration;
import com.arcadedb.GlobalConfiguration;
import com.arcadedb.log.LogManager;
import com.arcadedb.network.binary.SocketFactory;
import com.arcadedb.server.ArcadeDBServer;
import com.arcadedb.server.ServerException;
import com.arcadedb.server.ServerPlugin;
import com.arcadedb.server.http.handler.GetDatabasesHandler;
import com.arcadedb.server.http.handler.GetDynamicContentHandler;
import com.arcadedb.server.http.handler.GetExistsDatabaseHandler;
import com.arcadedb.server.http.handler.GetQueryHandler;
import com.arcadedb.server.http.handler.GetReadyHandler;
import com.arcadedb.server.http.handler.GetServerHandler;
import com.arcadedb.server.http.handler.PostBeginHandler;
import com.arcadedb.server.http.handler.PostCommandHandler;
import com.arcadedb.server.http.handler.PostCommitHandler;
import com.arcadedb.server.http.handler.PostQueryHandler;
import com.arcadedb.server.http.handler.PostRollbackHandler;
import com.arcadedb.server.http.handler.PostServerCommandHandler;
import com.arcadedb.server.http.ssl.SslUtils;
import com.arcadedb.server.http.ssl.TlsProtocol;
import com.arcadedb.server.http.ws.WebSocketConnectionHandler;
import com.arcadedb.server.http.ws.WebSocketEventBus;
import com.arcadedb.server.security.ServerSecurityException;
import io.undertow.Handlers;
import io.undertow.Undertow;
import io.undertow.server.RoutingHandler;
import io.undertow.server.handlers.PathHandler;
import org.xnio.Options;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import java.io.IOException;
import java.net.BindException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.logging.Level;
import static com.arcadedb.GlobalConfiguration.NETWORK_SSL_KEYSTORE;
import static com.arcadedb.GlobalConfiguration.NETWORK_SSL_KEYSTORE_PASSWORD;
import static com.arcadedb.GlobalConfiguration.NETWORK_SSL_TRUSTSTORE;
import static com.arcadedb.GlobalConfiguration.NETWORK_SSL_TRUSTSTORE_PASSWORD;
import static com.arcadedb.server.http.ssl.KeystoreType.JKS;
import static com.arcadedb.server.http.ssl.KeystoreType.PKCS12;
import static io.undertow.UndertowOptions.SHUTDOWN_TIMEOUT;
public class HttpServer implements ServerPlugin {
private final ArcadeDBServer server;
private final HttpSessionManager sessionManager;
private final WebSocketEventBus webSocketEventBus;
private Undertow undertow;
private volatile String listeningAddress;
private int httpPortListening;
public HttpServer(final ArcadeDBServer server) {
this.server = server;
this.sessionManager = new HttpSessionManager(
server.getConfiguration().getValueAsInteger(GlobalConfiguration.SERVER_HTTP_SESSION_EXPIRE_TIMEOUT) * 1_000L);
this.webSocketEventBus = new WebSocketEventBus(this.server);
}
@Override
public void stopService() {
webSocketEventBus.stop();
if (undertow != null) {
try {
undertow.stop();
} catch (final Exception e) {
// IGNORE IT
}
}
sessionManager.close();
}
@Override
public void startService() {
final ContextConfiguration configuration = server.getConfiguration();
final String host = configuration.getValueAsString(GlobalConfiguration.SERVER_HTTP_INCOMING_HOST);
final Object configuredHTTPPort = configuration.getValue(GlobalConfiguration.SERVER_HTTP_INCOMING_PORT);
final int[] httpPortRange = extractPortRange(configuredHTTPPort);
final Object configuredHTTPSPort = configuration.getValue(GlobalConfiguration.SERVER_HTTPS_INCOMING_PORT);
final int[] httpsPortRange =
configuredHTTPSPort != null && !configuredHTTPSPort.toString().isEmpty() ? extractPortRange(configuredHTTPSPort) : null;
LogManager.instance()
.log(this, Level.INFO, "- Starting HTTP Server (host=%s port=%s httpsPort=%s)...", host, configuredHTTPPort,
httpsPortRange != null ? configuredHTTPSPort : "-");
final PathHandler routes = new PathHandler();
final RoutingHandler basicRoutes = Handlers.routing();
routes.addPrefixPath("/ws", new WebSocketConnectionHandler(this, webSocketEventBus));
routes.addPrefixPath("/api/v1",//
basicRoutes//
.post("/begin/{database}", new PostBeginHandler(this))//
.post("/command/{database}", new PostCommandHandler(this))//
.post("/commit/{database}", new PostCommitHandler(this))//
.get("/databases", new GetDatabasesHandler(this))//
.get("/exists/{database}", new GetExistsDatabaseHandler(this))//
.get("/query/{database}/{language}/{command}", new GetQueryHandler(this))//
.post("/query/{database}", new PostQueryHandler(this))//
.post("/rollback/{database}", new PostRollbackHandler(this))//
.get("/server", new GetServerHandler(this))//
.post("/server", new PostServerCommandHandler(this))//
.get("/ready", new GetReadyHandler(this))//
);
if (!"production".equals(GlobalConfiguration.SERVER_MODE.getValueAsString())) {
routes.addPrefixPath("/", Handlers.routing().setFallbackHandler(new GetDynamicContentHandler(this)));
}
// REGISTER PLUGIN API
for (final ServerPlugin plugin : server.getPlugins()) {
plugin.registerAPI(this, routes);
}
int httpsPortListening = httpsPortRange != null ? httpsPortRange[0] : 0;
for (httpPortListening = httpPortRange[0]; httpPortListening <= httpPortRange[1]; ++httpPortListening) {
try {
final Undertow.Builder builder = Undertow.builder()//
.addHttpListener(httpPortListening, host)//
.setHandler(routes)//
.setSocketOption(Options.READ_TIMEOUT, configuration.getValueAsInteger(GlobalConfiguration.NETWORK_SOCKET_TIMEOUT))
.setIoThreads(configuration.getValueAsInteger(GlobalConfiguration.SERVER_HTTP_IO_THREADS))//
.setWorkerThreads(500)//
.setServerOption(SHUTDOWN_TIMEOUT, 5000);
if (configuration.getValueAsBoolean(GlobalConfiguration.NETWORK_USE_SSL)) {
final SSLContext sslContext = createSSLContext();
builder.addHttpsListener(httpsPortListening, host, sslContext);
}
undertow = builder.build();
undertow.start();
LogManager.instance().log(this, Level.INFO, "- HTTP Server started (host=%s port=%d httpsPort=%s)", host, httpPortListening,
httpsPortListening > 0 ? httpsPortListening : "-");
if (host.equals("0.0.0.0"))
listeningAddress = server.getHostAddress() + ":" + httpPortListening;
else
listeningAddress = host + ":" + httpPortListening;
return;
} catch (final Exception e) {
undertow = null;
if (e.getCause() instanceof BindException) {
// RETRY
LogManager.instance().log(this, Level.WARNING, "- HTTP Port %s not available", httpPortListening);
if (httpsPortListening > 0) {
++httpsPortListening;
}
continue;
}
throw new ServerException("Error on starting HTTP Server", e);
}
}
httpPortListening = -1;
final String msg = String.format("Unable to listen to a HTTP port in the configured port range %d - %d", httpPortRange[0],
httpPortRange[1]);
LogManager.instance().log(this, Level.SEVERE, msg);
throw new ServerException("Error on starting HTTP Server: " + msg);
}
private int[] extractPortRange(final Object configuredPort) {
int portFrom;
int portTo;
if (configuredPort instanceof Number) {
portFrom = portTo = ((Number) configuredPort).intValue();
} else {
final String[] parts = configuredPort.toString().split("-");
if (parts.length > 2) {
throw new IllegalArgumentException("Invalid format for http server port range");
} else if (parts.length == 1) {
portFrom = portTo = Integer.parseInt(parts[0]);
} else {
portFrom = Integer.parseInt(parts[0]);
portTo = Integer.parseInt(parts[1]);
}
}
return new int[] { portFrom, portTo };
}
public HttpSessionManager getSessionManager() {
return sessionManager;
}
public ArcadeDBServer getServer() {
return server;
}
public String getListeningAddress() {
return listeningAddress;
}
public int getPort() {
return httpPortListening;
}
public WebSocketEventBus getWebSocketEventBus() {
return webSocketEventBus;
}
private SSLContext createSSLContext() throws Exception {
ContextConfiguration configuration = server.getConfiguration();
String keystorePath = validateStoreProperty(configuration, NETWORK_SSL_KEYSTORE, "SSL key store path is empty");
String keystorePassword = validateStoreProperty(configuration, NETWORK_SSL_KEYSTORE_PASSWORD,
"SSL key store password is empty");
String truststorePath = validateStoreProperty(configuration, NETWORK_SSL_TRUSTSTORE, "SSL trust store path is empty");
String truststorePassword = validateStoreProperty(configuration, NETWORK_SSL_TRUSTSTORE_PASSWORD,
"SSL trust store password is empty");
KeyStore keyStore = configureSSLForKeystore(keystorePath, keystorePassword);
KeyStore trustStore = configureSSLForTruststore(truststorePath, truststorePassword);
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, keystorePassword.toCharArray());
KeyManager[] keyManagers = keyManagerFactory.getKeyManagers();
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(trustStore);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
SSLContext sslContext = SSLContext.getInstance(TlsProtocol.getLatestTlsVersion().getTlsVersion());
sslContext.init(keyManagers, trustManagers, null);
return sslContext;
}
private KeyStore configureSSLForKeystore(String keystorePath, String keystorePassword)
throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException {
return SslUtils.loadKeystoreFromStream(SocketFactory.getAsStream(keystorePath), keystorePassword,
SslUtils.getDefaultKeystoreTypeForKeystore(() -> PKCS12));
}
private KeyStore configureSSLForTruststore(String truststorePath, String truststorePassword)
throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException {
return SslUtils.loadKeystoreFromStream(SocketFactory.getAsStream(truststorePath), truststorePassword,
SslUtils.getDefaultKeystoreTypeForTruststore(() -> JKS));
}
private String validateStoreProperty(ContextConfiguration contextConfiguration, GlobalConfiguration configurationKey,
String errorMessage) {
String storePropertyValue = contextConfiguration.getValueAsString(configurationKey);
if ((storePropertyValue == null) || storePropertyValue.isEmpty()) {
throw new ServerSecurityException(errorMessage);
}
return storePropertyValue;
}
public boolean isConnected() {
return undertow != null && listeningAddress != null;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy