diff --git a/Q-Apps.md b/Q-Apps.md index de97200d..1b4f33bb 100644 --- a/Q-Apps.md +++ b/Q-Apps.md @@ -583,14 +583,15 @@ let res = await qortalRequest({ ``` ### Send foreign coin to address -_Requires user approval_ +_Requires user approval_
+Note: default fees can be found [here](https://github.com/Qortal/qortal-ui/blob/master/plugins/plugins/core/qdn/browser/browser.src.js#L205-L209). ``` let res = await qortalRequest({ action: "SEND_COIN", coin: "LTC", destinationAddress: "LSdTvMHRm8sScqwCi6x9wzYQae8JeZhx6y", amount: 1.00000000, // 1 LTC - fee: 0.00000020 // fee per byte + fee: 0.00000020 // Optional fee per byte (default fee used if omitted, recommended) - not used for QORT or ARRR }); ``` diff --git a/pom.xml b/pom.xml index c9986fd4..7c7ac147 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.qortal qortal - 4.1.2 + 4.1.3 jar true diff --git a/src/main/java/org/qortal/api/ApiService.java b/src/main/java/org/qortal/api/ApiService.java index 059b8971..1ee733c6 100644 --- a/src/main/java/org/qortal/api/ApiService.java +++ b/src/main/java/org/qortal/api/ApiService.java @@ -96,7 +96,7 @@ public class ApiService { throw new RuntimeException("Failed to start SSL API due to broken keystore"); // BouncyCastle-specific SSLContext build - SSLContext sslContext = SSLContext.getInstance("TLS", "BCJSSE"); + SSLContext sslContext = SSLContext.getInstance("TLSv1.3", "BCJSSE"); KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("PKIX", "BCJSSE"); KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType(), "BC"); diff --git a/src/main/java/org/qortal/api/DevProxyService.java b/src/main/java/org/qortal/api/DevProxyService.java new file mode 100644 index 00000000..e0bf02db --- /dev/null +++ b/src/main/java/org/qortal/api/DevProxyService.java @@ -0,0 +1,173 @@ +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.network.Network; +import org.qortal.repository.DataException; +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 DevProxyService { + + private static DevProxyService instance; + + private final ResourceConfig config; + private Server server; + + private DevProxyService() { + this.config = new ResourceConfig(); + this.config.packages("org.qortal.api.proxy.resource", "org.qortal.api.resource"); + this.config.register(OpenApiResource.class); + this.config.register(ApiDefinition.class); + this.config.register(AnnotationPostProcessor.class); + } + + public static DevProxyService getInstance() { + if (instance == null) + instance = new DevProxyService(); + + return instance; + } + + public Iterable> getResources() { + return this.config.getClasses(); + } + + public void start() throws DataException { + 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().getDevProxyPort()); + + 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(Network.getInstance().getBindAddress()); + portUnifiedConnector.setPort(Settings.getInstance().getDevProxyPort()); + + this.server.addConnector(portUnifiedConnector); + } else { + // Non-SSL + InetAddress bindAddr = InetAddress.getByName(Network.getInstance().getBindAddress()); + InetSocketAddress endpoint = new InetSocketAddress(bindAddr, Settings.getInstance().getDevProxyPort()); + this.server = new Server(endpoint); + } + + // Error handler + ErrorHandler errorHandler = new ApiErrorHandler(); + this.server.setErrorHandler(errorHandler); + + // Request logging + if (Settings.getInstance().isDevProxyLoggingEnabled()) { + RequestLogWriter logWriter = new RequestLogWriter("devproxy-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 DataException("Failed to start developer proxy", e); + } + } + + public void stop() { + try { + // Stop server + this.server.stop(); + } catch (Exception e) { + // Failed to stop + } + + this.server = null; + instance = null; + } + +} diff --git a/src/main/java/org/qortal/api/DomainMapService.java b/src/main/java/org/qortal/api/DomainMapService.java index 3b81d94c..8b791121 100644 --- a/src/main/java/org/qortal/api/DomainMapService.java +++ b/src/main/java/org/qortal/api/DomainMapService.java @@ -69,7 +69,7 @@ public class DomainMapService { throw new RuntimeException("Failed to start SSL API due to broken keystore"); // BouncyCastle-specific SSLContext build - SSLContext sslContext = SSLContext.getInstance("TLS", "BCJSSE"); + SSLContext sslContext = SSLContext.getInstance("TLSv1.3", "BCJSSE"); KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("PKIX", "BCJSSE"); KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType(), "BC"); diff --git a/src/main/java/org/qortal/api/GatewayService.java b/src/main/java/org/qortal/api/GatewayService.java index 51191af3..24a7b7c9 100644 --- a/src/main/java/org/qortal/api/GatewayService.java +++ b/src/main/java/org/qortal/api/GatewayService.java @@ -69,7 +69,7 @@ public class GatewayService { throw new RuntimeException("Failed to start SSL API due to broken keystore"); // BouncyCastle-specific SSLContext build - SSLContext sslContext = SSLContext.getInstance("TLS", "BCJSSE"); + SSLContext sslContext = SSLContext.getInstance("TLSv1.3", "BCJSSE"); KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("PKIX", "BCJSSE"); KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType(), "BC"); diff --git a/src/main/java/org/qortal/api/HTMLParser.java b/src/main/java/org/qortal/api/HTMLParser.java index cc3102e8..f1794594 100644 --- a/src/main/java/org/qortal/api/HTMLParser.java +++ b/src/main/java/org/qortal/api/HTMLParser.java @@ -24,11 +24,11 @@ public class HTMLParser { private String theme; private boolean usingCustomRouting; - public HTMLParser(String resourceId, String inPath, String prefix, boolean usePrefix, byte[] data, + public HTMLParser(String resourceId, String inPath, String prefix, boolean includeResourceIdInPrefix, byte[] data, String qdnContext, Service service, String identifier, String theme, boolean usingCustomRouting) { - String inPathWithoutFilename = inPath.contains("/") ? inPath.substring(0, inPath.lastIndexOf('/')) : ""; - this.qdnBase = usePrefix ? String.format("%s/%s", prefix, resourceId) : ""; - this.qdnBaseWithPath = usePrefix ? String.format("%s/%s%s", prefix, resourceId, inPathWithoutFilename) : ""; + String inPathWithoutFilename = inPath.contains("/") ? inPath.substring(0, inPath.lastIndexOf('/')) : String.format("/%s",inPath); + this.qdnBase = includeResourceIdInPrefix ? String.format("%s/%s", prefix, resourceId) : prefix; + this.qdnBaseWithPath = includeResourceIdInPrefix ? String.format("%s/%s%s", prefix, resourceId, inPathWithoutFilename) : String.format("%s%s", prefix, inPathWithoutFilename); this.data = data; this.qdnContext = qdnContext; this.resourceId = resourceId; @@ -82,7 +82,7 @@ public class HTMLParser { } public static boolean isHtmlFile(String path) { - if (path.endsWith(".html") || path.endsWith(".htm")) { + if (path.endsWith(".html") || path.endsWith(".htm") || path.equals("")) { return true; } return false; diff --git a/src/main/java/org/qortal/api/domainmap/resource/DomainMapResource.java b/src/main/java/org/qortal/api/domainmap/resource/DomainMapResource.java index 4cb9f8e5..019fb753 100644 --- a/src/main/java/org/qortal/api/domainmap/resource/DomainMapResource.java +++ b/src/main/java/org/qortal/api/domainmap/resource/DomainMapResource.java @@ -48,10 +48,10 @@ public class DomainMapResource { } private HttpServletResponse get(String resourceId, ResourceIdType resourceIdType, Service service, String identifier, - String inPath, String secret58, String prefix, boolean usePrefix, boolean async) { + String inPath, String secret58, String prefix, boolean includeResourceIdInPrefix, boolean async) { ArbitraryDataRenderer renderer = new ArbitraryDataRenderer(resourceId, resourceIdType, service, identifier, inPath, - secret58, prefix, usePrefix, async, "domainMap", request, response, context); + secret58, prefix, includeResourceIdInPrefix, async, "domainMap", request, response, context); return renderer.render(); } diff --git a/src/main/java/org/qortal/api/gateway/resource/GatewayResource.java b/src/main/java/org/qortal/api/gateway/resource/GatewayResource.java index 15bb398e..d82fb98d 100644 --- a/src/main/java/org/qortal/api/gateway/resource/GatewayResource.java +++ b/src/main/java/org/qortal/api/gateway/resource/GatewayResource.java @@ -76,7 +76,7 @@ public class GatewayResource { } - private HttpServletResponse parsePath(String inPath, String qdnContext, String secret58, boolean usePrefix, boolean async) { + private HttpServletResponse parsePath(String inPath, String qdnContext, String secret58, boolean includeResourceIdInPrefix, boolean async) { if (inPath == null || inPath.equals("")) { // Assume not a real file @@ -143,7 +143,7 @@ public class GatewayResource { } ArbitraryDataRenderer renderer = new ArbitraryDataRenderer(name, ResourceIdType.NAME, service, identifier, outPath, - secret58, prefix, usePrefix, async, qdnContext, request, response, context); + secret58, prefix, includeResourceIdInPrefix, async, qdnContext, request, response, context); return renderer.render(); } diff --git a/src/main/java/org/qortal/api/proxy/resource/DevProxyServerResource.java b/src/main/java/org/qortal/api/proxy/resource/DevProxyServerResource.java new file mode 100644 index 00000000..d51e6852 --- /dev/null +++ b/src/main/java/org/qortal/api/proxy/resource/DevProxyServerResource.java @@ -0,0 +1,142 @@ +package org.qortal.api.proxy.resource; + +import org.qortal.api.ApiError; +import org.qortal.api.ApiExceptionFactory; +import org.qortal.api.HTMLParser; +import org.qortal.arbitrary.misc.Service; +import org.qortal.controller.DevProxyManager; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.core.Context; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Enumeration; + + +@Path("/") +public class DevProxyServerResource { + + @Context HttpServletRequest request; + @Context HttpServletResponse response; + @Context ServletContext context; + + + @GET + public HttpServletResponse getProxyIndex() { + return this.proxy("/"); + } + + @GET + @Path("{path:.*}") + public HttpServletResponse getProxyPath(@PathParam("path") String inPath) { + return this.proxy(inPath); + } + + private HttpServletResponse proxy(String inPath) { + try { + String source = DevProxyManager.getInstance().getSourceHostAndPort(); + + // Convert localhost / 127.0.0.1 to IPv6 [::1] + if (source.startsWith("localhost") || source.startsWith("127.0.0.1")) { + int port = 80; + String[] parts = source.split(":"); + if (parts.length > 1) { + port = Integer.parseInt(parts[1]); + } + source = String.format("[::1]:%d", port); + } + + if (!inPath.startsWith("/")) { + inPath = "/" + inPath; + } + + String queryString = request.getQueryString() != null ? "?" + request.getQueryString() : ""; + + // Open URL + String urlString = String.format("http://%s%s%s", source, inPath, queryString); + URL url = new URL(urlString); + HttpURLConnection con = (HttpURLConnection) url.openConnection(); + con.setRequestMethod(request.getMethod()); + + // Proxy the request headers + Enumeration headerNames = request.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String headerName = headerNames.nextElement(); + String headerValue = request.getHeader(headerName); + con.setRequestProperty(headerName, headerValue); + } + + // TODO: proxy any POST parameters from "request" to "con" + + // Proxy the response code + int responseCode = con.getResponseCode(); + response.setStatus(responseCode); + + // Proxy the response headers + for (int i = 0; ; i++) { + String headerKey = con.getHeaderFieldKey(i); + String headerValue = con.getHeaderField(i); + if (headerKey != null && headerValue != null) { + response.addHeader(headerKey, headerValue); + continue; + } + break; + } + + // Read the response body + InputStream inputStream = con.getInputStream(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + byte[] data = outputStream.toByteArray(); // TODO: limit file size that can be read into memory + + // Close the streams + outputStream.close(); + inputStream.close(); + + // Extract filename + String filename = ""; + if (inPath.contains("/")) { + String[] parts = inPath.split("/"); + if (parts.length > 0) { + filename = parts[parts.length - 1]; + } + } + + // Parse and modify output if needed + if (HTMLParser.isHtmlFile(filename)) { + // HTML file - needs to be parsed + HTMLParser htmlParser = new HTMLParser("", inPath, "", false, data, "proxy", Service.APP, null, "light", true); + htmlParser.addAdditionalHeaderTags(); + response.addHeader("Content-Security-Policy", "default-src 'self' 'unsafe-inline' 'unsafe-eval'; media-src 'self' data: blob:; img-src 'self' data: blob:; connect-src 'self' ws:; font-src 'self' data:;"); + response.setContentType(con.getContentType()); + response.setContentLength(htmlParser.getData().length); + response.getOutputStream().write(htmlParser.getData()); + } + else { + // Regular file - can be streamed directly + response.addHeader("Content-Security-Policy", "default-src 'self'"); + response.setContentType(con.getContentType()); + response.setContentLength(data.length); + response.getOutputStream().write(data); + } + + } catch (IOException e) { + throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, e.getMessage()); + } + + return response; + } + +} diff --git a/src/main/java/org/qortal/api/resource/DeveloperResource.java b/src/main/java/org/qortal/api/resource/DeveloperResource.java new file mode 100644 index 00000000..ba534502 --- /dev/null +++ b/src/main/java/org/qortal/api/resource/DeveloperResource.java @@ -0,0 +1,96 @@ +package org.qortal.api.resource; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.qortal.api.ApiError; +import org.qortal.api.ApiErrors; +import org.qortal.api.ApiExceptionFactory; +import org.qortal.controller.DevProxyManager; +import org.qortal.repository.DataException; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; + + +@Path("/developer") +@Tag(name = "Developer Tools") +public class DeveloperResource { + + @Context HttpServletRequest request; + @Context HttpServletResponse response; + @Context ServletContext context; + + + @POST + @Path("/proxy/start") + @Operation( + summary = "Start proxy server, for real time QDN app/website development", + requestBody = @RequestBody( + description = "Host and port of source webserver to be proxied", + required = true, + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "string", + example = "127.0.0.1:5173" + ) + ) + ), + responses = { + @ApiResponse( + description = "Port number of running server", + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "number" + ) + ) + ) + } + ) + @ApiErrors({ApiError.INVALID_CRITERIA}) + public Integer startProxy(String sourceHostAndPort) { + // TODO: API key + DevProxyManager devProxyManager = DevProxyManager.getInstance(); + try { + devProxyManager.setSourceHostAndPort(sourceHostAndPort); + devProxyManager.start(); + return devProxyManager.getPort(); + + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, e.getMessage()); + } + } + + @POST + @Path("/proxy/stop") + @Operation( + summary = "Stop proxy server", + responses = { + @ApiResponse( + description = "true if stopped", + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "boolean" + ) + ) + ) + } + ) + public boolean stopProxy() { + DevProxyManager devProxyManager = DevProxyManager.getInstance(); + devProxyManager.stop(); + return !devProxyManager.isRunning(); + } + +} \ No newline at end of file diff --git a/src/main/java/org/qortal/api/restricted/resource/RenderResource.java b/src/main/java/org/qortal/api/restricted/resource/RenderResource.java index 7a772f9f..92f72032 100644 --- a/src/main/java/org/qortal/api/restricted/resource/RenderResource.java +++ b/src/main/java/org/qortal/api/restricted/resource/RenderResource.java @@ -157,10 +157,10 @@ public class RenderResource { private HttpServletResponse get(String resourceId, ResourceIdType resourceIdType, Service service, String identifier, - String inPath, String secret58, String prefix, boolean usePrefix, boolean async, String theme) { + String inPath, String secret58, String prefix, boolean includeResourceIdInPrefix, boolean async, String theme) { ArbitraryDataRenderer renderer = new ArbitraryDataRenderer(resourceId, resourceIdType, service, identifier, inPath, - secret58, prefix, usePrefix, async, "render", request, response, context); + secret58, prefix, includeResourceIdInPrefix, async, "render", request, response, context); if (theme != null) { renderer.setTheme(theme); diff --git a/src/main/java/org/qortal/arbitrary/ArbitraryDataRenderer.java b/src/main/java/org/qortal/arbitrary/ArbitraryDataRenderer.java index 089a99ca..704533c8 100644 --- a/src/main/java/org/qortal/arbitrary/ArbitraryDataRenderer.java +++ b/src/main/java/org/qortal/arbitrary/ArbitraryDataRenderer.java @@ -40,7 +40,7 @@ public class ArbitraryDataRenderer { private String inPath; private final String secret58; private final String prefix; - private final boolean usePrefix; + private final boolean includeResourceIdInPrefix; private final boolean async; private final String qdnContext; private final HttpServletRequest request; @@ -48,7 +48,7 @@ public class ArbitraryDataRenderer { private final ServletContext context; public ArbitraryDataRenderer(String resourceId, ResourceIdType resourceIdType, Service service, String identifier, - String inPath, String secret58, String prefix, boolean usePrefix, boolean async, String qdnContext, + String inPath, String secret58, String prefix, boolean includeResourceIdInPrefix, boolean async, String qdnContext, HttpServletRequest request, HttpServletResponse response, ServletContext context) { this.resourceId = resourceId; @@ -58,7 +58,7 @@ public class ArbitraryDataRenderer { this.inPath = inPath; this.secret58 = secret58; this.prefix = prefix; - this.usePrefix = usePrefix; + this.includeResourceIdInPrefix = includeResourceIdInPrefix; this.async = async; this.qdnContext = qdnContext; this.request = request; @@ -159,7 +159,7 @@ public class ArbitraryDataRenderer { if (HTMLParser.isHtmlFile(filename)) { // HTML file - needs to be parsed byte[] data = Files.readAllBytes(filePath); // TODO: limit file size that can be read into memory - HTMLParser htmlParser = new HTMLParser(resourceId, inPath, prefix, usePrefix, data, qdnContext, service, identifier, theme, usingCustomRouting); + HTMLParser htmlParser = new HTMLParser(resourceId, inPath, prefix, includeResourceIdInPrefix, data, qdnContext, service, identifier, theme, usingCustomRouting); htmlParser.addAdditionalHeaderTags(); response.addHeader("Content-Security-Policy", "default-src 'self' 'unsafe-inline' 'unsafe-eval'; media-src 'self' data: blob:; img-src 'self' data: blob:;"); response.setContentType(context.getMimeType(filename)); diff --git a/src/main/java/org/qortal/controller/DevProxyManager.java b/src/main/java/org/qortal/controller/DevProxyManager.java new file mode 100644 index 00000000..a04e87ac --- /dev/null +++ b/src/main/java/org/qortal/controller/DevProxyManager.java @@ -0,0 +1,74 @@ +package org.qortal.controller; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.qortal.api.DevProxyService; +import org.qortal.repository.DataException; +import org.qortal.settings.Settings; + +public class DevProxyManager { + + protected static final Logger LOGGER = LogManager.getLogger(DevProxyManager.class); + + private static DevProxyManager instance; + + private boolean running = false; + + private String sourceHostAndPort = "127.0.0.1:5173"; // Default for React/Vite + + private DevProxyManager() { + + } + + public static DevProxyManager getInstance() { + if (instance == null) + instance = new DevProxyManager(); + + return instance; + } + + public void start() throws DataException { + synchronized(this) { + if (this.running) { + // Already running + return; + } + + LOGGER.info(String.format("Starting developer proxy service on port %d", Settings.getInstance().getDevProxyPort())); + DevProxyService devProxyService = DevProxyService.getInstance(); + devProxyService.start(); + this.running = true; + } + } + + public void stop() { + synchronized(this) { + if (!this.running) { + // Not running + return; + } + + LOGGER.info(String.format("Shutting down developer proxy service")); + DevProxyService devProxyService = DevProxyService.getInstance(); + devProxyService.stop(); + this.running = false; + } + } + + public void setSourceHostAndPort(String sourceHostAndPort) { + this.sourceHostAndPort = sourceHostAndPort; + } + + public String getSourceHostAndPort() { + return this.sourceHostAndPort; + } + + public Integer getPort() { + return Settings.getInstance().getDevProxyPort(); + } + + public boolean isRunning() { + return this.running; + } + +} diff --git a/src/main/java/org/qortal/crypto/TrustlessSSLSocketFactory.java b/src/main/java/org/qortal/crypto/TrustlessSSLSocketFactory.java index aba1955e..f723e651 100644 --- a/src/main/java/org/qortal/crypto/TrustlessSSLSocketFactory.java +++ b/src/main/java/org/qortal/crypto/TrustlessSSLSocketFactory.java @@ -28,7 +28,7 @@ public abstract class TrustlessSSLSocketFactory { private static final SSLContext sc; static { try { - sc = SSLContext.getInstance("SSL"); + sc = SSLContext.getInstance("TLSv1.3"); sc.init(null, TRUSTLESS_MANAGER, new java.security.SecureRandom()); } catch (Exception e) { throw new RuntimeException(e); diff --git a/src/main/java/org/qortal/network/Handshake.java b/src/main/java/org/qortal/network/Handshake.java index 4500cd59..341f4e21 100644 --- a/src/main/java/org/qortal/network/Handshake.java +++ b/src/main/java/org/qortal/network/Handshake.java @@ -265,7 +265,7 @@ public enum Handshake { private static final long PEER_VERSION_131 = 0x0100030001L; /** Minimum peer version that we are allowed to communicate with */ - private static final String MIN_PEER_VERSION = "4.0.0"; + private static final String MIN_PEER_VERSION = "4.1.1"; private static final int POW_BUFFER_SIZE_PRE_131 = 8 * 1024 * 1024; // bytes private static final int POW_DIFFICULTY_PRE_131 = 8; // leading zero bits diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java index 362227a5..c3d5a0c8 100644 --- a/src/main/java/org/qortal/settings/Settings.java +++ b/src/main/java/org/qortal/settings/Settings.java @@ -47,6 +47,9 @@ public class Settings { private static final int MAINNET_GATEWAY_PORT = 80; private static final int TESTNET_GATEWAY_PORT = 8080; + private static final int MAINNET_DEV_PROXY_PORT = 12393; + private static final int TESTNET_DEV_PROXY_PORT = 62393; + private static final Logger LOGGER = LogManager.getLogger(Settings.class); private static final String SETTINGS_FILENAME = "settings.json"; @@ -107,6 +110,11 @@ public class Settings { private boolean gatewayLoggingEnabled = false; private boolean gatewayLoopbackEnabled = false; + // Developer Proxy + private Integer devProxyPort; + private boolean devProxyLoggingEnabled = false; + + // Specific to this node private boolean wipeUnconfirmedOnStart = false; /** Maximum number of unconfirmed transactions allowed per account */ @@ -219,7 +227,7 @@ public class Settings { public long recoveryModeTimeout = 24 * 60 * 60 * 1000L; /** Minimum peer version number required in order to sync with them */ - private String minPeerVersion = "4.1.1"; + private String minPeerVersion = "4.1.2"; /** Whether to allow connections with peers below minPeerVersion * If true, we won't sync with them but they can still sync with us, and will show in the peers list * If false, sync will be blocked both ways, and they will not appear in the peers list */ @@ -649,6 +657,18 @@ public class Settings { } + public int getDevProxyPort() { + if (this.devProxyPort != null) + return this.devProxyPort; + + return this.isTestNet ? TESTNET_DEV_PROXY_PORT : MAINNET_DEV_PROXY_PORT; + } + + public boolean isDevProxyLoggingEnabled() { + return this.devProxyLoggingEnabled; + } + + public boolean getWipeUnconfirmedOnStart() { return this.wipeUnconfirmedOnStart; } diff --git a/src/main/resources/i18n/ApiError_jp.properties b/src/main/resources/i18n/ApiError_jp.properties new file mode 100644 index 00000000..603914cb --- /dev/null +++ b/src/main/resources/i18n/ApiError_jp.properties @@ -0,0 +1,83 @@ +#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/) +# Keys are from api.ApiError enum + +# "localeLang": "jp", + +### Common ### +JSON = JSON メッセージの解析に失敗しました + +INSUFFICIENT_BALANCE = 残高不足 + +UNAUTHORIZED = APIコール未承認 + +REPOSITORY_ISSUE = リポジトリエラー + +NON_PRODUCTION = この APIコールはプロダクションシステムでは許可されていません + +BLOCKCHAIN_NEEDS_SYNC = ブロックチェーンをまず同期する必要があります + +NO_TIME_SYNC = 時刻が未同期 + +### Validation ### +INVALID_SIGNATURE = 無効な署名 + +INVALID_ADDRESS = 無効なアドレス + +INVALID_PUBLIC_KEY = 無効な公開鍵 + +INVALID_DATA = 無効なデータ + +INVALID_NETWORK_ADDRESS = 無効なネットワーク アドレス + +ADDRESS_UNKNOWN = 不明なアカウントアドレス + +INVALID_CRITERIA = 無効な検索条件 + +INVALID_REFERENCE = 無効な参照 + +TRANSFORMATION_ERROR = JSONをトランザクションに変換出来ませんでした + +INVALID_PRIVATE_KEY = 無効な秘密鍵 + +INVALID_HEIGHT = 無効なブロック高 + +CANNOT_MINT = アカウントはミント出来ません + +### Blocks ### +BLOCK_UNKNOWN = 不明なブロック + +### Transactions ### +TRANSACTION_UNKNOWN = 不明なトランザクション + +PUBLIC_KEY_NOT_FOUND = 公開鍵が見つかりません + +# this one is special in that caller expected to pass two additional strings, hence the two %s +TRANSACTION_INVALID = 無効なトランザクション: %s (%s) + +### Naming ### +NAME_UNKNOWN = 不明な名前 + +### Asset ### +INVALID_ASSET_ID = 無効なアセット ID + +INVALID_ORDER_ID = 無効なアセット注文 ID + +ORDER_UNKNOWN = 不明なアセット注文 ID + +### Groups ### +GROUP_UNKNOWN = 不明なグループ + +### Foreign Blockchain ### +FOREIGN_BLOCKCHAIN_NETWORK_ISSUE = 外部ブロックチェーンまたはElectrumXネットワークの問題 + +FOREIGN_BLOCKCHAIN_BALANCE_ISSUE = 外部ブロックチェーンの残高が不足しています + +FOREIGN_BLOCKCHAIN_TOO_SOON = 外部ブロックチェーン トランザクションのブロードキャストが時期尚早 (ロックタイム/ブロック時間の中央値) + +### Trade Portal ### +ORDER_SIZE_TOO_SMALL = 注文金額が低すぎます + +### Data ### +FILE_NOT_FOUND = ファイルが見つかりません + +NO_REPLY = ピアが制限時間内に応答しませんでした diff --git a/src/main/resources/i18n/SysTray_jp.properties b/src/main/resources/i18n/SysTray_jp.properties new file mode 100644 index 00000000..c4cccb5b --- /dev/null +++ b/src/main/resources/i18n/SysTray_jp.properties @@ -0,0 +1,48 @@ +#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/) +# SysTray pop-up menu # Japanese translation by R M 2023 + +APPLYING_UPDATE_AND_RESTARTING = 自動更新を適用して再起動しています... + +AUTO_UPDATE = 自動更新 + +BLOCK_HEIGHT = ブロック高 + +BLOCKS_REMAINING = 残りのブロック + +BUILD_VERSION = ビルドバージョン + +CHECK_TIME_ACCURACY = 時刻の精度を確認 + +CONNECTING = 接続中 + +CONNECTION = 接続 + +CONNECTIONS = 接続 + +CREATING_BACKUP_OF_DB_FILES = データベース ファイルのバックアップを作成中... + +DB_BACKUP = データベースのバックアップ + +DB_CHECKPOINT = データベースのチェックポイント + +DB_MAINTENANCE = データベースのメンテナンス + +EXIT = 終了 + +LITE_NODE = ライトノード + +MINTING_DISABLED = ミント一時中止中 + +MINTING_ENABLED = \u2714 ミント + +OPEN_UI = UIを開く + +PERFORMING_DB_CHECKPOINT = コミットされていないデータベースの変更を保存中... + +PERFORMING_DB_MAINTENANCE = 定期メンテナンスを実行中... + +SYNCHRONIZE_CLOCK = 時刻を同期 + +SYNCHRONIZING_BLOCKCHAIN = ブロックチェーンを同期中 + +SYNCHRONIZING_CLOCK = 時刻を同期中 diff --git a/src/main/resources/i18n/TransactionValidity_jp.properties b/src/main/resources/i18n/TransactionValidity_jp.properties new file mode 100644 index 00000000..9540372a --- /dev/null +++ b/src/main/resources/i18n/TransactionValidity_jp.properties @@ -0,0 +1,195 @@ +# + +ACCOUNT_ALREADY_EXISTS = 既にアカウントは存在します + +ACCOUNT_CANNOT_REWARD_SHARE = アカウントは報酬シェアが出来ません + +ADDRESS_ABOVE_RATE_LIMIT = アドレスが指定されたレート制限に達しました + +ADDRESS_BLOCKED = このアドレスはブロックされています + +ALREADY_GROUP_ADMIN = 既ににグループ管理者です + +ALREADY_GROUP_MEMBER = 既にグループメンバーです + +ALREADY_VOTED_FOR_THAT_OPTION = 既にそのオプションに投票しています + +ASSET_ALREADY_EXISTS = 既にアセットは存在します + +ASSET_DOES_NOT_EXIST = アセットが存在しません + +ASSET_DOES_NOT_MATCH_AT = アセットがATのアセットと一致しません + +ASSET_NOT_SPENDABLE = 資産が使用不可です + +AT_ALREADY_EXISTS = 既にATが存在します + +AT_IS_FINISHED = ATが終了しました + +AT_UNKNOWN = 不明なAT + +BAN_EXISTS = 既にバンされてます + +BAN_UNKNOWN = 不明なバン + +BANNED_FROM_GROUP = グループからのバンされています + +BUYER_ALREADY_OWNER = 既に購入者が所有者です + +CLOCK_NOT_SYNCED = 時刻が未同期 + +DUPLICATE_MESSAGE = このアドレスは重複メッセージを送信しました + +DUPLICATE_OPTION = 重複したオプション + +GROUP_ALREADY_EXISTS = 既にグループは存在します + +GROUP_APPROVAL_DECIDED = 既にグループの承認は決定されています + +GROUP_APPROVAL_NOT_REQUIRED = グループ承認が不必要 + +GROUP_DOES_NOT_EXIST = グループが存在しません + +GROUP_ID_MISMATCH = グループ ID が不一致 + +GROUP_OWNER_CANNOT_LEAVE = グループ所有者はグループを退会出来ません + +HAVE_EQUALS_WANT = 持っている資産は欲しい資産と同じです + +INCORRECT_NONCE = 不正な PoW ナンス + +INSUFFICIENT_FEE = 手数料が不十分です + +INVALID_ADDRESS = 無効なアドレス + +INVALID_AMOUNT = 無効な金額 + +INVALID_ASSET_OWNER = 無効なアセット所有者 + +INVALID_AT_TRANSACTION = 無効なATトランザクション + +INVALID_AT_TYPE_LENGTH = 無効なATの「タイプ」の長さです + +INVALID_BUT_OK = 無効だがOK + +INVALID_CREATION_BYTES = 無効な作成バイト数 + +INVALID_DATA_LENGTH = 無効なデータ長 + +INVALID_DESCRIPTION_LENGTH = 無効な概要の長さ + +INVALID_GROUP_APPROVAL_THRESHOLD = 無効なグループ承認のしきい値 + +INVALID_GROUP_BLOCK_DELAY = 無効なグループ承認のブロック遅延 + +INVALID_GROUP_ID = 無効なグループ ID + +INVALID_GROUP_OWNER = 無効なグループ所有者 + +INVALID_LIFETIME = 無効な有効期間 + +INVALID_NAME_LENGTH = 無効な名前の長さです + +INVALID_NAME_OWNER = 無効な名前の所有者 + +INVALID_OPTION_LENGTH = 無効なオプションの長さ + +INVALID_OPTIONS_COUNT = 無効なオプションの数 + +INVALID_ORDER_CREATOR = 無効な注文作成者 + +INVALID_PAYMENTS_COUNT = 無効な入出金数 + +INVALID_PUBLIC_KEY = 無効な公開鍵 + +INVALID_QUANTITY = 無効な数量 + +INVALID_REFERENCE = 無効な参照 + +INVALID_RETURN = 無効な返品 + +INVALID_REWARD_SHARE_PERCENT = 無効な報酬シェア率 + +INVALID_SELLER = 無効な販売者 + +INVALID_TAGS_LENGTH = 無効な「タグ」の長さ + +INVALID_TIMESTAMP_SIGNATURE = 無効なタイムスタンプ署名 + +INVALID_TX_GROUP_ID = 無効なトランザクション グループ ID + +INVALID_VALUE_LENGTH = 無効な「値」の長さ + +INVITE_UNKNOWN = 不明なグループ招待 + +JOIN_REQUEST_EXISTS = 既にグループ参加リクエストが存在します + +MAXIMUM_REWARD_SHARES = 既にこのアカウントの報酬シェアは最大です + +MISSING_CREATOR = 作成者が見つかりません + +MULTIPLE_NAMES_FORBIDDEN = アカウントごとに複数の登録名は禁止されています + +NAME_ALREADY_FOR_SALE = 既に名前は販売中です + +NAME_ALREADY_REGISTERED = 既に名前は登録されています + +NAME_BLOCKED = この名前はブロックされています + +NAME_DOES_NOT_EXIST = 名前は存在しません + +NAME_NOT_FOR_SALE = 名前は非売品です + +NAME_NOT_NORMALIZED = 名前は Unicode の「正規化」形式ではありません + +NEGATIVE_AMOUNT = 無効な/負の金額 + +NEGATIVE_FEE = 無効な/負の料金 + +NEGATIVE_PRICE = 無効な/負の価格 + +NO_BALANCE = 残高が不足しています + +NO_BLOCKCHAIN_LOCK = ノードのブロックチェーンは現在ビジーです + +NO_FLAG_PERMISSION = アカウントにはその権限がありません + +NOT_GROUP_ADMIN = アカウントはグループ管理者ではありません + +NOT_GROUP_MEMBER = アカウントはグループメンバーではありません + +NOT_MINTING_ACCOUNT = アカウントはミント出来ません + +NOT_YET_RELEASED = 機能はまだリリースされていません + +OK = OK + +ORDER_ALREADY_CLOSED = 既に資産取引注文は終了しています + +ORDER_DOES_NOT_EXIST = 資産取引注文が存在しません + +POLL_ALREADY_EXISTS = 既に投票は存在します + +POLL_DOES_NOT_EXIST = 投票は存在しません + +POLL_OPTION_DOES_NOT_EXIST = 投票オプションが存在しません + +PUBLIC_KEY_UNKNOWN = 不明な公開鍵 + +REWARD_SHARE_UNKNOWN = 不明な報酬シェア + +SELF_SHARE_EXISTS = 既に自己シェア(報酬シェア)が存在します + +TIMESTAMP_TOO_NEW = タイムスタンプが新しすぎます + +TIMESTAMP_TOO_OLD = タイムスタンプが古すぎます + +TOO_MANY_UNCONFIRMED = アカウントに保留中の未承認トランザクションが多すぎます + +TRANSACTION_ALREADY_CONFIRMED = 既にトランザクションは承認されています + +TRANSACTION_ALREADY_EXISTS = 既にトランザクションは存在します + +TRANSACTION_UNKNOWN = 不明なトランザクション + +TX_GROUP_ID_MISMATCH = トランザクションのグループIDが一致しません diff --git a/src/main/resources/q-apps/q-apps.js b/src/main/resources/q-apps/q-apps.js index e766fffd..6cc1bf08 100644 --- a/src/main/resources/q-apps/q-apps.js +++ b/src/main/resources/q-apps/q-apps.js @@ -472,6 +472,10 @@ function getDefaultTimeout(action) { // Allow extra time for other actions that create transactions, even if there is no PoW return 5 * 60 * 1000; + case "GET_WALLET_BALANCE": + // Getting a wallet balance can take a while, if there are many transactions + return 2 * 60 * 1000; + default: break; }