forked from Qortal/qortal
Added gateway service, to allow websites to be served directly on a domain or IP.
This replaces the existing GET /site/{name} API endpoints. Example settings: "gatewayServiceEnabled": true, "gatewayServicePort": 80 Websites can then be served using URL: http://localhost/RegisteredName Or, if node is behind public DNS: http://example.com/RegisteredName Or, if a custom port (such as 12393) is used: http://localhost:12393/RegisteredName http://example.com:12393/RegisteredName This is currently for serving websites only, but can be adapted to serve other services if needed.
This commit is contained in:
parent
c0fedaa3a4
commit
c8b70b51c3
170
src/main/java/org/qortal/api/GatewayService.java
Normal file
170
src/main/java/org/qortal/api/GatewayService.java
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
package org.qortal.api;
|
||||||
|
|
||||||
|
import io.swagger.v3.jaxrs2.integration.resources.OpenApiResource;
|
||||||
|
import org.eclipse.jetty.http.HttpVersion;
|
||||||
|
import org.eclipse.jetty.rewrite.handler.RewriteHandler;
|
||||||
|
import org.eclipse.jetty.server.*;
|
||||||
|
import org.eclipse.jetty.server.handler.ErrorHandler;
|
||||||
|
import org.eclipse.jetty.server.handler.InetAccessHandler;
|
||||||
|
import org.eclipse.jetty.servlet.FilterHolder;
|
||||||
|
import org.eclipse.jetty.servlet.ServletContextHandler;
|
||||||
|
import org.eclipse.jetty.servlet.ServletHolder;
|
||||||
|
import org.eclipse.jetty.servlets.CrossOriginFilter;
|
||||||
|
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||||
|
import org.glassfish.jersey.server.ResourceConfig;
|
||||||
|
import org.glassfish.jersey.servlet.ServletContainer;
|
||||||
|
import org.qortal.api.resource.AnnotationPostProcessor;
|
||||||
|
import org.qortal.api.resource.ApiDefinition;
|
||||||
|
import org.qortal.settings.Settings;
|
||||||
|
|
||||||
|
import javax.net.ssl.KeyManagerFactory;
|
||||||
|
import javax.net.ssl.SSLContext;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.security.KeyStore;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
|
public class GatewayService {
|
||||||
|
|
||||||
|
private static GatewayService instance;
|
||||||
|
|
||||||
|
private final ResourceConfig config;
|
||||||
|
private Server server;
|
||||||
|
|
||||||
|
private GatewayService() {
|
||||||
|
this.config = new ResourceConfig();
|
||||||
|
this.config.packages("org.qortal.api.gateway.resource");
|
||||||
|
this.config.register(OpenApiResource.class);
|
||||||
|
this.config.register(ApiDefinition.class);
|
||||||
|
this.config.register(AnnotationPostProcessor.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GatewayService getInstance() {
|
||||||
|
if (instance == null)
|
||||||
|
instance = new GatewayService();
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Iterable<Class<?>> getResources() {
|
||||||
|
return this.config.getClasses();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start() {
|
||||||
|
try {
|
||||||
|
// Create API server
|
||||||
|
|
||||||
|
// SSL support if requested
|
||||||
|
String keystorePathname = Settings.getInstance().getSslKeystorePathname();
|
||||||
|
String keystorePassword = Settings.getInstance().getSslKeystorePassword();
|
||||||
|
|
||||||
|
if (keystorePathname != null && keystorePassword != null) {
|
||||||
|
// SSL version
|
||||||
|
if (!Files.isReadable(Path.of(keystorePathname)))
|
||||||
|
throw new RuntimeException("Failed to start SSL API due to broken keystore");
|
||||||
|
|
||||||
|
// BouncyCastle-specific SSLContext build
|
||||||
|
SSLContext sslContext = SSLContext.getInstance("TLS", "BCJSSE");
|
||||||
|
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("PKIX", "BCJSSE");
|
||||||
|
|
||||||
|
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType(), "BC");
|
||||||
|
|
||||||
|
try (InputStream keystoreStream = Files.newInputStream(Paths.get(keystorePathname))) {
|
||||||
|
keyStore.load(keystoreStream, keystorePassword.toCharArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
keyManagerFactory.init(keyStore, keystorePassword.toCharArray());
|
||||||
|
sslContext.init(keyManagerFactory.getKeyManagers(), null, new SecureRandom());
|
||||||
|
|
||||||
|
SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
|
||||||
|
sslContextFactory.setSslContext(sslContext);
|
||||||
|
|
||||||
|
this.server = new Server();
|
||||||
|
|
||||||
|
HttpConfiguration httpConfig = new HttpConfiguration();
|
||||||
|
httpConfig.setSecureScheme("https");
|
||||||
|
httpConfig.setSecurePort(Settings.getInstance().getGatewayServicePort());
|
||||||
|
|
||||||
|
SecureRequestCustomizer src = new SecureRequestCustomizer();
|
||||||
|
httpConfig.addCustomizer(src);
|
||||||
|
|
||||||
|
HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory(httpConfig);
|
||||||
|
SslConnectionFactory sslConnectionFactory = new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString());
|
||||||
|
|
||||||
|
ServerConnector portUnifiedConnector = new ServerConnector(this.server,
|
||||||
|
new DetectorConnectionFactory(sslConnectionFactory),
|
||||||
|
httpConnectionFactory);
|
||||||
|
portUnifiedConnector.setHost(Settings.getInstance().getBindAddress());
|
||||||
|
portUnifiedConnector.setPort(Settings.getInstance().getGatewayServicePort());
|
||||||
|
|
||||||
|
this.server.addConnector(portUnifiedConnector);
|
||||||
|
} else {
|
||||||
|
// Non-SSL
|
||||||
|
InetAddress bindAddr = InetAddress.getByName(Settings.getInstance().getBindAddress());
|
||||||
|
InetSocketAddress endpoint = new InetSocketAddress(bindAddr, Settings.getInstance().getGatewayServicePort());
|
||||||
|
this.server = new Server(endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error handler
|
||||||
|
ErrorHandler errorHandler = new ApiErrorHandler();
|
||||||
|
this.server.setErrorHandler(errorHandler);
|
||||||
|
|
||||||
|
// Request logging
|
||||||
|
if (Settings.getInstance().isGatewayLoggingEnabled()) {
|
||||||
|
RequestLogWriter logWriter = new RequestLogWriter("gateway-requests.log");
|
||||||
|
logWriter.setAppend(true);
|
||||||
|
logWriter.setTimeZone("UTC");
|
||||||
|
RequestLog requestLog = new CustomRequestLog(logWriter, CustomRequestLog.EXTENDED_NCSA_FORMAT);
|
||||||
|
this.server.setRequestLog(requestLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Access handler (currently no whitelist is used)
|
||||||
|
InetAccessHandler accessHandler = new InetAccessHandler();
|
||||||
|
this.server.setHandler(accessHandler);
|
||||||
|
|
||||||
|
// URL rewriting
|
||||||
|
RewriteHandler rewriteHandler = new RewriteHandler();
|
||||||
|
accessHandler.setHandler(rewriteHandler);
|
||||||
|
|
||||||
|
// Context
|
||||||
|
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
|
||||||
|
context.setContextPath("/");
|
||||||
|
rewriteHandler.setHandler(context);
|
||||||
|
|
||||||
|
// Cross-origin resource sharing
|
||||||
|
FilterHolder corsFilterHolder = new FilterHolder(CrossOriginFilter.class);
|
||||||
|
corsFilterHolder.setInitParameter(CrossOriginFilter.ALLOWED_ORIGINS_PARAM, "*");
|
||||||
|
corsFilterHolder.setInitParameter(CrossOriginFilter.ALLOWED_METHODS_PARAM, "GET, POST, DELETE");
|
||||||
|
corsFilterHolder.setInitParameter(CrossOriginFilter.CHAIN_PREFLIGHT_PARAM, "false");
|
||||||
|
context.addFilter(corsFilterHolder, "/*", null);
|
||||||
|
|
||||||
|
// API servlet
|
||||||
|
ServletContainer container = new ServletContainer(this.config);
|
||||||
|
ServletHolder apiServlet = new ServletHolder(container);
|
||||||
|
apiServlet.setInitOrder(1);
|
||||||
|
context.addServlet(apiServlet, "/*");
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
this.server.start();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Failed to start
|
||||||
|
throw new RuntimeException("Failed to start API", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
try {
|
||||||
|
// Stop server
|
||||||
|
this.server.stop();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Failed to stop
|
||||||
|
}
|
||||||
|
|
||||||
|
this.server = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
package org.qortal.api.gateway.resource;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import org.qortal.api.Security;
|
||||||
|
import org.qortal.arbitrary.ArbitraryDataFile.ResourceIdType;
|
||||||
|
import org.qortal.arbitrary.ArbitraryDataRenderer;
|
||||||
|
import org.qortal.arbitrary.misc.Service;
|
||||||
|
|
||||||
|
import javax.servlet.ServletContext;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import javax.ws.rs.*;
|
||||||
|
import javax.ws.rs.core.Context;
|
||||||
|
|
||||||
|
|
||||||
|
@Path("/")
|
||||||
|
@Tag(name = "Gateway")
|
||||||
|
public class GatewayResource {
|
||||||
|
|
||||||
|
@Context HttpServletRequest request;
|
||||||
|
@Context HttpServletResponse response;
|
||||||
|
@Context ServletContext context;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("{name}/{path:.*}")
|
||||||
|
@SecurityRequirement(name = "apiKey")
|
||||||
|
public HttpServletResponse getPathByName(@PathParam("name") String name,
|
||||||
|
@PathParam("path") String inPath) {
|
||||||
|
Security.checkApiCallAllowed(request);
|
||||||
|
return this.get(name, ResourceIdType.NAME, Service.WEBSITE, inPath, null, "", true, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("{name}")
|
||||||
|
@SecurityRequirement(name = "apiKey")
|
||||||
|
public HttpServletResponse getIndexByName(@PathParam("name") String name) {
|
||||||
|
Security.checkApiCallAllowed(request);
|
||||||
|
return this.get(name, ResourceIdType.NAME, Service.WEBSITE, "/", null, "", true, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpServletResponse get(String resourceId, ResourceIdType resourceIdType, Service service, String inPath,
|
||||||
|
String secret58, String prefix, boolean usePrefix, boolean async) {
|
||||||
|
|
||||||
|
ArbitraryDataRenderer renderer = new ArbitraryDataRenderer(resourceId, resourceIdType, service, inPath,
|
||||||
|
secret58, prefix, usePrefix, async, request, response, context);
|
||||||
|
return renderer.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -87,7 +87,7 @@ public class ArbitraryDataRenderer {
|
|||||||
String unzippedPath = path.toString();
|
String unzippedPath = path.toString();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String filename = this.getFilename(unzippedPath.toString(), inPath);
|
String filename = this.getFilename(unzippedPath, inPath);
|
||||||
String filePath = unzippedPath + File.separator + filename;
|
String filePath = unzippedPath + File.separator + filename;
|
||||||
|
|
||||||
if (HTMLParser.isHtmlFile(filename)) {
|
if (HTMLParser.isHtmlFile(filename)) {
|
||||||
|
@ -46,6 +46,7 @@ import org.qortal.account.PrivateKeyAccount;
|
|||||||
import org.qortal.account.PublicKeyAccount;
|
import org.qortal.account.PublicKeyAccount;
|
||||||
import org.qortal.api.ApiService;
|
import org.qortal.api.ApiService;
|
||||||
import org.qortal.api.DomainMapService;
|
import org.qortal.api.DomainMapService;
|
||||||
|
import org.qortal.api.GatewayService;
|
||||||
import org.qortal.block.Block;
|
import org.qortal.block.Block;
|
||||||
import org.qortal.block.BlockChain;
|
import org.qortal.block.BlockChain;
|
||||||
import org.qortal.block.BlockChain.BlockTimingByHeight;
|
import org.qortal.block.BlockChain.BlockTimingByHeight;
|
||||||
@ -507,6 +508,19 @@ public class Controller extends Thread {
|
|||||||
return; // Not System.exit() so that GUI can display error
|
return; // Not System.exit() so that GUI can display error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Settings.getInstance().isGatewayServiceEnabled()) {
|
||||||
|
LOGGER.info(String.format("Starting gateway service on port %d", Settings.getInstance().getGatewayServicePort()));
|
||||||
|
try {
|
||||||
|
GatewayService gatewayService = GatewayService.getInstance();
|
||||||
|
gatewayService.start();
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error("Unable to start gateway service", e);
|
||||||
|
Controller.getInstance().shutdown();
|
||||||
|
Gui.getInstance().fatalError("Gateway service failure", e);
|
||||||
|
return; // Not System.exit() so that GUI can display error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (Settings.getInstance().isDomainMapServiceEnabled()) {
|
if (Settings.getInstance().isDomainMapServiceEnabled()) {
|
||||||
LOGGER.info(String.format("Starting domain map service on port %d", Settings.getInstance().getDomainMapServicePort()));
|
LOGGER.info(String.format("Starting domain map service on port %d", Settings.getInstance().getDomainMapServicePort()));
|
||||||
try {
|
try {
|
||||||
|
@ -41,6 +41,9 @@ public class Settings {
|
|||||||
private static final int MAINNET_DOMAIN_MAP_SERVICE_PORT = 80;
|
private static final int MAINNET_DOMAIN_MAP_SERVICE_PORT = 80;
|
||||||
private static final int TESTNET_DOMAIN_MAP_SERVICE_PORT = 8080;
|
private static final int TESTNET_DOMAIN_MAP_SERVICE_PORT = 8080;
|
||||||
|
|
||||||
|
private static final int MAINNET_GATEWAY_SERVICE_PORT = 80;
|
||||||
|
private static final int TESTNET_GATEWAY_SERVICE_PORT = 8080;
|
||||||
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger(Settings.class);
|
private static final Logger LOGGER = LogManager.getLogger(Settings.class);
|
||||||
private static final String SETTINGS_FILENAME = "settings.json";
|
private static final String SETTINGS_FILENAME = "settings.json";
|
||||||
|
|
||||||
@ -93,6 +96,11 @@ public class Settings {
|
|||||||
private boolean domainMapLoggingEnabled = false;
|
private boolean domainMapLoggingEnabled = false;
|
||||||
private List<DomainMap> domainMap = null;
|
private List<DomainMap> domainMap = null;
|
||||||
|
|
||||||
|
// Gateway
|
||||||
|
private Integer gatewayServicePort;
|
||||||
|
private boolean gatewayServiceEnabled = false;
|
||||||
|
private boolean gatewayLoggingEnabled = false;
|
||||||
|
|
||||||
// Specific to this node
|
// Specific to this node
|
||||||
private boolean wipeUnconfirmedOnStart = false;
|
private boolean wipeUnconfirmedOnStart = false;
|
||||||
/** Maximum number of unconfirmed transactions allowed per account */
|
/** Maximum number of unconfirmed transactions allowed per account */
|
||||||
@ -538,6 +546,23 @@ public class Settings {
|
|||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public int getGatewayServicePort() {
|
||||||
|
if (this.gatewayServicePort != null)
|
||||||
|
return this.gatewayServicePort;
|
||||||
|
|
||||||
|
return this.isTestNet ? TESTNET_GATEWAY_SERVICE_PORT : MAINNET_GATEWAY_SERVICE_PORT;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isGatewayServiceEnabled() {
|
||||||
|
return this.gatewayServiceEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isGatewayLoggingEnabled() {
|
||||||
|
return this.gatewayLoggingEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public boolean getWipeUnconfirmedOnStart() {
|
public boolean getWipeUnconfirmedOnStart() {
|
||||||
return this.wipeUnconfirmedOnStart;
|
return this.wipeUnconfirmedOnStart;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user