mirror of https://github.com/qortal/qortal
Browse Source
Domain names can be mapped to arbitrary transaction signatures via the node's settings, and then served over port 80 or 443. This allows Qortal hosted sites to be accessible via a traditional domain name. Example configuration to map two domains: "domainMapServiceEnabled": true, "domainMapServicePort": 80, "domainMap": [ { "domain": "example.com", "signature": "tEsw4kUn4ZJfPha7CotUL6BHkFPs79BwKXdY6yrf28YTpDn4KSY6ZKX3nwZCkqDF9RyXbgaVnB1rTEExY3h9CQA" }, { "domain": "demo.qortal.org", "signature": "ZdBWWPMhR7AZwSu5xZm89mQEacekqkNfAimSCqFP6rQGKaGnXR9G4SWYpY5awFGfhmNBWzvRnXkWZKCsj6EMgc8" } ] Each domain needs to be pointed to the Qortal data node via an A record or CNAME. You can add redundant nodes by adding multiple A records for the same domain (this is known as DNS Failover). Note that running a webserver on port 80 (or anything less than 1024) requires running the data node as root. There are workarounds to this, such as disabling privileged ports, or using a reverse proxy. I will investigate this more as time goes on, but this is okay for a proof of concept.qdn
CalDescent
3 years ago
4 changed files with 274 additions and 7 deletions
@ -0,0 +1,174 @@
|
||||
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.rewrite.handler.RewritePatternRule; |
||||
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 DomainMapService { |
||||
|
||||
private static DomainMapService instance; |
||||
|
||||
private final ResourceConfig config; |
||||
private Server server; |
||||
|
||||
private DomainMapService() { |
||||
this.config = new ResourceConfig(); |
||||
this.config.packages("org.qortal.api.resource"); |
||||
this.config.register(OpenApiResource.class); |
||||
this.config.register(ApiDefinition.class); |
||||
this.config.register(AnnotationPostProcessor.class); |
||||
} |
||||
|
||||
public static DomainMapService getInstance() { |
||||
if (instance == null) |
||||
instance = new DomainMapService(); |
||||
|
||||
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().getDomainMapServicePort()); |
||||
|
||||
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().getDomainMapServicePort()); |
||||
|
||||
this.server.addConnector(portUnifiedConnector); |
||||
} else { |
||||
// Non-SSL
|
||||
InetAddress bindAddr = InetAddress.getByName(Settings.getInstance().getBindAddress()); |
||||
InetSocketAddress endpoint = new InetSocketAddress(bindAddr, Settings.getInstance().getDomainMapServicePort()); |
||||
this.server = new Server(endpoint); |
||||
} |
||||
|
||||
// Error handler
|
||||
ErrorHandler errorHandler = new ApiErrorHandler(); |
||||
this.server.setErrorHandler(errorHandler); |
||||
|
||||
// Request logging
|
||||
if (Settings.getInstance().isDomainMapLoggingEnabled()) { |
||||
RequestLogWriter logWriter = new RequestLogWriter("domainmap-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, "/*"); |
||||
|
||||
// Rewrite URLs
|
||||
rewriteHandler.addRule(new RewritePatternRule("/*", "/site/domainmap/")); // rewrite / as /site/domainmap/
|
||||
|
||||
// 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; |
||||
} |
||||
|
||||
} |
Loading…
Reference in new issue