diff --git a/pom.xml b/pom.xml index a2a790fa..798d68ea 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.qortal qortal - 3.0.2 + 3.0.4 jar true diff --git a/src/main/java/org/qortal/ApplyUpdate.java b/src/main/java/org/qortal/ApplyUpdate.java index edd6d924..90171191 100644 --- a/src/main/java/org/qortal/ApplyUpdate.java +++ b/src/main/java/org/qortal/ApplyUpdate.java @@ -7,14 +7,13 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.security.Security; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; +import java.util.*; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; +import org.qortal.api.ApiKey; import org.qortal.api.ApiRequest; import org.qortal.controller.AutoUpdate; import org.qortal.settings.Settings; @@ -70,14 +69,40 @@ public class ApplyUpdate { String baseUri = "http://localhost:" + Settings.getInstance().getApiPort() + "/"; LOGGER.info(() -> String.format("Shutting down node using API via %s", baseUri)); + // The /admin/stop endpoint requires an API key, which may or may not be already generated + boolean apiKeyNewlyGenerated = false; + ApiKey apiKey = null; + try { + apiKey = new ApiKey(); + if (!apiKey.generated()) { + apiKey.generate(); + apiKeyNewlyGenerated = true; + LOGGER.info("Generated API key"); + } + } catch (IOException e) { + LOGGER.info("Error loading API key: {}", e.getMessage()); + } + + // Create GET params + Map params = new HashMap<>(); + if (apiKey != null) { + params.put("apiKey", apiKey.toString()); + } + + // Attempt to stop the node int attempt; for (attempt = 0; attempt < MAX_ATTEMPTS; ++attempt) { final int attemptForLogging = attempt; LOGGER.info(() -> String.format("Attempt #%d out of %d to shutdown node", attemptForLogging + 1, MAX_ATTEMPTS)); - String response = ApiRequest.perform(baseUri + "admin/stop", null); - if (response == null) + String response = ApiRequest.perform(baseUri + "admin/stop", params); + if (response == null) { // No response - consider node shut down + if (apiKeyNewlyGenerated) { + // API key was newly generated for this auto update, so we need to remove it + ApplyUpdate.removeGeneratedApiKey(); + } return true; + } LOGGER.info(() -> String.format("Response from API: %s", response)); @@ -89,6 +114,11 @@ public class ApplyUpdate { } } + if (apiKeyNewlyGenerated) { + // API key was newly generated for this auto update, so we need to remove it + ApplyUpdate.removeGeneratedApiKey(); + } + if (attempt == MAX_ATTEMPTS) { LOGGER.error("Failed to shutdown node - giving up"); return false; @@ -97,6 +127,19 @@ public class ApplyUpdate { return true; } + private static void removeGeneratedApiKey() { + try { + LOGGER.info("Removing newly generated API key..."); + + // Delete the API key since it was only generated for this auto update + ApiKey apiKey = new ApiKey(); + apiKey.delete(); + + } catch (IOException e) { + LOGGER.info("Error loading or deleting API key: {}", e.getMessage()); + } + } + private static void replaceJar() { // Assuming current working directory contains the JAR files Path realJar = Paths.get(JAR_FILENAME); diff --git a/src/main/java/org/qortal/api/ApiKey.java b/src/main/java/org/qortal/api/ApiKey.java index 6a79dd20..3f7cfe35 100644 --- a/src/main/java/org/qortal/api/ApiKey.java +++ b/src/main/java/org/qortal/api/ApiKey.java @@ -81,6 +81,15 @@ public class ApiKey { writer.close(); } + public void delete() throws IOException { + this.apiKey = null; + + Path filePath = this.getFilePath(); + if (Files.exists(filePath)) { + Files.delete(filePath); + } + } + public boolean generated() { return (this.apiKey != null); diff --git a/src/main/java/org/qortal/api/resource/ArbitraryResource.java b/src/main/java/org/qortal/api/resource/ArbitraryResource.java index 82618152..d542b89c 100644 --- a/src/main/java/org/qortal/api/resource/ArbitraryResource.java +++ b/src/main/java/org/qortal/api/resource/ArbitraryResource.java @@ -575,7 +575,11 @@ public class ArbitraryResource { @PathParam("name") String name, @QueryParam("filepath") String filepath, @QueryParam("rebuild") boolean rebuild) { - Security.checkApiCallAllowed(request); + + // Authentication can be bypassed in the settings, for those running public QDN nodes + if (!Settings.getInstance().isQDNAuthBypassEnabled()) { + Security.checkApiCallAllowed(request); + } return this.download(service, name, null, filepath, rebuild); } @@ -604,7 +608,11 @@ public class ArbitraryResource { @PathParam("identifier") String identifier, @QueryParam("filepath") String filepath, @QueryParam("rebuild") boolean rebuild) { - Security.checkApiCallAllowed(request); + + // Authentication can be bypassed in the settings, for those running public QDN nodes + if (!Settings.getInstance().isQDNAuthBypassEnabled()) { + Security.checkApiCallAllowed(request); + } return this.download(service, name, identifier, filepath, rebuild); } @@ -1049,6 +1057,10 @@ public class ArbitraryResource { // This is a single file resource filepath = files[0]; } + else { + throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, + "filepath is required for resources containing more than one file"); + } } // TODO: limit file size that can be read into memory diff --git a/src/main/java/org/qortal/arbitrary/ArbitraryDataResource.java b/src/main/java/org/qortal/arbitrary/ArbitraryDataResource.java index 65c92cc6..0ece14a5 100644 --- a/src/main/java/org/qortal/arbitrary/ArbitraryDataResource.java +++ b/src/main/java/org/qortal/arbitrary/ArbitraryDataResource.java @@ -6,6 +6,7 @@ import org.qortal.arbitrary.ArbitraryDataFile.ResourceIdType; import org.qortal.arbitrary.misc.Service; import org.qortal.controller.arbitrary.ArbitraryDataBuildManager; import org.qortal.controller.arbitrary.ArbitraryDataManager; +import org.qortal.controller.arbitrary.ArbitraryDataStorageManager; import org.qortal.data.arbitrary.ArbitraryResourceStatus; import org.qortal.data.transaction.ArbitraryTransactionData; import org.qortal.list.ResourceListManager; @@ -116,6 +117,9 @@ public class ArbitraryDataResource { // Also delete cached data for the entire resource this.deleteCache(); + // Invalidate the hosted transactions cache as we have removed an item + ArbitraryDataStorageManager.getInstance().invalidateHostedTransactionsCache(); + return true; } catch (DataException | IOException e) { diff --git a/src/main/java/org/qortal/controller/BlockMinter.java b/src/main/java/org/qortal/controller/BlockMinter.java index 4eea91a9..a20cf9ae 100644 --- a/src/main/java/org/qortal/controller/BlockMinter.java +++ b/src/main/java/org/qortal/controller/BlockMinter.java @@ -51,7 +51,7 @@ public class BlockMinter extends Thread { // Min account level to submit blocks // This is an unvalidated version of Blockchain.minAccountLevelToMint // and exists only to reduce block candidates by default. - private static int MIN_LEVEL_FOR_BLOCK_SUBMISSION = 3; + private static int MIN_LEVEL_FOR_BLOCK_SUBMISSION = 6; // Constructors diff --git a/src/main/java/org/qortal/list/ResourceList.java b/src/main/java/org/qortal/list/ResourceList.java index fbdc8470..099aa168 100644 --- a/src/main/java/org/qortal/list/ResourceList.java +++ b/src/main/java/org/qortal/list/ResourceList.java @@ -13,6 +13,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Collections; import java.util.List; public class ResourceList { @@ -20,7 +21,7 @@ public class ResourceList { private static final Logger LOGGER = LogManager.getLogger(ResourceList.class); private String name; - private List list = new ArrayList<>(); + private List list = Collections.synchronizedList(new ArrayList<>()); /** * ResourceList diff --git a/src/main/java/org/qortal/list/ResourceListManager.java b/src/main/java/org/qortal/list/ResourceListManager.java index 4d4d559d..4182f87c 100644 --- a/src/main/java/org/qortal/list/ResourceListManager.java +++ b/src/main/java/org/qortal/list/ResourceListManager.java @@ -5,6 +5,7 @@ import org.apache.logging.log4j.Logger; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Objects; @@ -13,7 +14,7 @@ public class ResourceListManager { private static final Logger LOGGER = LogManager.getLogger(ResourceListManager.class); private static ResourceListManager instance; - private List lists = new ArrayList<>(); + private List lists = Collections.synchronizedList(new ArrayList<>()); public ResourceListManager() { diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBArbitraryRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBArbitraryRepository.java index 0087ce23..a7da66ae 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBArbitraryRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBArbitraryRepository.java @@ -330,7 +330,7 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository { bindParams.add(name); } - sql.append(" GROUP BY name, service, identifier ORDER BY name"); + sql.append(" GROUP BY name, service, identifier ORDER BY name COLLATE SQL_TEXT_UCC_NO_PAD"); if (reverse != null && reverse) { sql.append(" DESC"); @@ -401,7 +401,7 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository { bindParams.add(queryWildcard); } - sql.append(" GROUP BY name, service, identifier ORDER BY name"); + sql.append(" GROUP BY name, service, identifier ORDER BY name COLLATE SQL_TEXT_UCC_NO_PAD"); if (reverse != null && reverse) { sql.append(" DESC"); @@ -465,7 +465,7 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository { sql.append(" AND (identifier = ? OR (? IS NULL))"); } - sql.append(" GROUP BY name ORDER BY name"); + sql.append(" GROUP BY name ORDER BY name COLLATE SQL_TEXT_UCC_NO_PAD"); if (reverse != null && reverse) { sql.append(" DESC"); diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java index 3bd7cef5..41b69114 100644 --- a/src/main/java/org/qortal/settings/Settings.java +++ b/src/main/java/org/qortal/settings/Settings.java @@ -308,6 +308,9 @@ public class Settings { /** Maximum total size of hosted data, in bytes. Unlimited if null */ private Long maxStorageCapacity = null; + /** Whether to serve QDN data without authentication */ + private boolean qdnAuthBypassEnabled = false; + // Domain mapping public static class DomainMap { private String domain; @@ -884,4 +887,8 @@ public class Settings { public Long getMaxStorageCapacity() { return this.maxStorageCapacity; } + + public boolean isQDNAuthBypassEnabled() { + return this.qdnAuthBypassEnabled; + } } diff --git a/src/test/java/org/qortal/test/api/AdminApiTests.java b/src/test/java/org/qortal/test/api/AdminApiTests.java index 89b1464a..b3e6da03 100644 --- a/src/test/java/org/qortal/test/api/AdminApiTests.java +++ b/src/test/java/org/qortal/test/api/AdminApiTests.java @@ -2,10 +2,12 @@ package org.qortal.test.api; import static org.junit.Assert.*; +import org.apache.commons.lang3.reflect.FieldUtils; import org.junit.Before; import org.junit.Test; import org.qortal.api.resource.AdminResource; import org.qortal.repository.DataException; +import org.qortal.settings.Settings; import org.qortal.test.common.ApiCommon; import org.qortal.test.common.Common; @@ -29,7 +31,10 @@ public class AdminApiTests extends ApiCommon { } @Test - public void testSummary() { + public void testSummary() throws IllegalAccessException { + // Set localAuthBypassEnabled to true, since we don't need to test authentication here + FieldUtils.writeField(Settings.getInstance(), "localAuthBypassEnabled", true, true); + assertNotNull(this.adminResource.summary("testApiKey")); } diff --git a/src/test/resources/test-settings-v2.json b/src/test/resources/test-settings-v2.json index 7802f598..b2ad3db8 100644 --- a/src/test/resources/test-settings-v2.json +++ b/src/test/resources/test-settings-v2.json @@ -15,6 +15,5 @@ "tempDataPath": "data-test/_temp", "listsPath": "lists-test", "storagePolicy": "FOLLOWED_OR_VIEWED", - "maxStorageCapacity": 104857600, - "localAuthBypassEnabled": true + "maxStorageCapacity": 104857600 }