Compare commits

...

44 Commits
qdn ... v3.0.3

Author SHA1 Message Date
CalDescent
311fe98f44 Bump version to 3.0.3 2022-01-23 13:19:49 +00:00
CalDescent
6f7c8d96b9 Removed ApiService instance creation in ApplyUpdate as it wasn't really needed, and probably not sensible to instantiate it here. 2022-01-23 12:57:28 +00:00
CalDescent
ff6ec83b1c Removed localAuthBypassEnabled override in unit tests.
Hopefully this will allow us to proactively catch any missing API keys in the future.
2022-01-23 12:48:37 +00:00
CalDescent
ea10eec926 Fixed bug in auto update process - use the API key when stopping the node.
Luckily this code is included in the new JAR, not the old one, so we should be able to regain auto update ability by issuing a new update.
2022-01-23 12:38:19 +00:00
CalDescent
6f724f648d Fixed testDirectoryDigest() which has been failing for a couple of versions (due to gitignore removing the cache file) 2022-01-22 20:48:58 +00:00
CalDescent
048776e090 Ignore failing test due to recent API update, which makes the test incompatible. To be fixed later. 2022-01-22 20:43:28 +00:00
CalDescent
a7c02733ec Updated approve-auto-update.sh to use new service format 2022-01-22 19:40:13 +00:00
CalDescent
59346db427 Bump version to 3.0.2 2022-01-22 18:45:32 +00:00
CalDescent
25efee55b8 Added networking optimization, to avoid wasted processing on every read.
Thanks to @catbref for finding this.
2022-01-22 18:43:32 +00:00
CalDescent
b30445c5f8 Increase MAX_ACCOUNT_COUNT to 5000 (around 0.5MB of data) to see if it helps with minting efficiency. 2022-01-21 16:11:21 +00:00
CalDescent
d105613e51 Improvements to some /blocks API endpoints 2022-01-21 16:07:18 +00:00
CalDescent
ef43e78d54 Reduced log spam 2022-01-21 15:20:16 +00:00
CalDescent
6f61fbb127 Reduced strictness of rate limiter, to allow two additional file list requests (15 and 30 seconds after initial attempt)
This should help catch any remaining chunks that were unable to be fetched in the first attempt due to network failures, bad peers, etc.
2022-01-21 15:20:04 +00:00
CalDescent
9f9b7cab99 Include "size" in /arbitrary/resource APIs
Note that this is not always accurate - it relates to the largest transaction size for this name, not necessarily the latest or the combined size of multiple transactions. This can be made accurate as soon as we have a "Resources" table to store this info. Trying to do it before then will be too inefficient in terms of queries.
2022-01-21 14:42:37 +00:00
CalDescent
f129e16878 Added retry mechanism to relay file deletions, just in case it fails on the first try.
A longer term solution to this hypothetical problem is to store relays in RAM or a temp folder only. Or maybe add an indicator file to instruct the cleanup manager to delete it. But this will require more development. 10 deletion attempts (each 1 second apart) should be enough for now.
2022-01-21 14:32:15 +00:00
CalDescent
8a42dce763 Use the first responding peer in the relay map.
This encourages shorter relays, since longer ones will take more time to respond, and also prevents a peer from intentionally taking a long time to respond so that it overwrites an existing entry.

Longer term we could consider keeping track of all respondents for each hash, if there are still issues with data retrieval. I suspect this won't be needed though, as the requesting peer has 16+ different peers connected, and therefore potentially 16 different mappings already.
2022-01-21 14:23:55 +00:00
CalDescent
6423d5e474 Optimized onNetworkGetArbitraryDataFileListMessage() to remove duplicate calls to Files.exists() 2022-01-21 14:12:59 +00:00
CalDescent
6e91157dcf Improved cache clearing process and logging. 2022-01-21 13:32:59 +00:00
CalDescent
85c61c1bc1 Added GET /arbitrary/resources/search API
Example usage

List all websites with a name containing the word "crow" (using default identifier):
http://localhost:12391/arbitrary/resources/search?service=WEBSITE&query=crow&default=true&limit=20

List all resources with name or identifier containing the word "crow":
http://localhost:12391/arbitrary/resources/search?query=crow&default=false&limit=20
2022-01-21 10:04:18 +00:00
CalDescent
54af36fb85 Remove duplicates in GET /arbitrary/hosted/resources response 2022-01-20 22:34:41 +00:00
CalDescent
fcdcc939e6 Sort hosted data in reverse order (newest first) 2022-01-20 22:29:19 +00:00
CalDescent
13450d5afa Added limit/offset to GET /arbitrary/hosted API endpoints 2022-01-20 22:28:28 +00:00
CalDescent
5e1e653095 Removed unnecessary database lookups in GET /hosted/resources API 2022-01-20 20:39:46 +00:00
CalDescent
e8fabcb449 Removed extra isDataLocal() check in GET /hosted/resources which was evading the cache. 2022-01-20 20:38:57 +00:00
CalDescent
a4ce41ed39 Updated qort and qdata to check for apikey.txt in $HOME/qortal, which is the most commonly installed location.
This allows the script to be run from any directory, as long as the core is installed at $HOME/qortal.
2022-01-20 20:31:16 +00:00
CalDescent
1b42062d57 Default minPeerVersion set to 3.0.1 2022-01-20 20:25:20 +00:00
CalDescent
c2a4b01a9c Allow local playback of media files 2022-01-20 20:25:06 +00:00
CalDescent
47e763b0cf Take a copy of the IP address history so it can be safely iterated. Without this, another thread could remove an element, resulting in an exception. 2022-01-20 20:24:51 +00:00
CalDescent
0278f6c9f2 Reduced more log spam 2022-01-20 20:24:26 +00:00
CalDescent
d96bc14516 Allow execution of inline scripts, at least for now. 2022-01-17 20:25:25 +00:00
CalDescent
318f433f22 Reduced log spam when checking for avatars. 2022-01-17 20:04:54 +00:00
CalDescent
cfc80cb9b0 Use a header instead of a meta tag for Content-Security-Policy, because we can't guarantee that we are parsing all HTML files.
Also use default-src instead of connect-src, as we want to block all external requests.
2022-01-17 20:04:35 +00:00
CalDescent
01c6149422 Restrict websites to same origin requests only, using a Content-Security-Policy meta tag. 2022-01-16 20:52:30 +00:00
CalDescent
6f80a6c08a Rework of file list requests and relays, allowing it to handle multiple chunk resources in a much more sensible way.
This could create a lot of additional relay traffic as a result, so needs lots of testing and possibly optimizing.
2022-01-16 20:39:37 +00:00
CalDescent
8fb2d38cd1 Revert "Revert log4j version for now. We need to put this back in the next update, once log4j2.properties files have transitioned to the new format."
This reverts commit 777bddd3d8.
2022-01-15 20:35:44 +00:00
CalDescent
5018d27c25 "Not started" renamed to "Published" 2022-01-15 20:21:52 +00:00
CalDescent
1d77101253 Use AES/CBC/PKCS5Padding for encryption, and fall back to just AES for legacy resource support.
Should fix "ECB mode cannot use IV" error due to mode and padding not being stated.
2022-01-15 20:14:32 +00:00
CalDescent
1ddd468c1f Added API key support to qdata script
As with the qort script, it currently needs to be run from either the qortal directory or the tools directory in order to pick up the API key
2022-01-14 11:46:26 +00:00
CalDescent
f05cd9ea51 Added API key support to qort script
Currently needs to be run from either the qortal directory or the tools directory in order to pick up the API key
2022-01-14 11:45:40 +00:00
CalDescent
70c00a4150 Updated AdvancedInstaller project for v3.0.1 2022-01-13 22:36:31 +00:00
CalDescent
d296029e8e Bump version to 3.0.1 2022-01-13 20:18:32 +00:00
CalDescent
e257fd8628 Updated stop.sh script to use the /admin/stop API endpoint if an apikey.txt file is available.
This brings the behaviour closer to the old version so should hopefully reduce the amount of newly introduced issues. If an API key is unavailable, it will fall back to using `kill -15 $pid` (i.e. a SIGTERM).
2022-01-13 19:18:39 +00:00
CalDescent
119c1b43be Use default values for method and compression if not specified.
Should fix issue with v4 transactions where these aren't used. Matches with the NOT NULL DEFAULT 0 which automatically transitions existing v4 ARBITRARY transactions to use the same defaults.
2022-01-13 19:09:00 +00:00
CalDescent
1277ce38de Bump version to 3.0.0 2022-01-12 21:11:02 +00:00
34 changed files with 541 additions and 131 deletions

View File

@@ -2,7 +2,7 @@
<DOCUMENT Type="Advanced Installer" CreateVersion="14.9" version="18.2" Modules="enterprise" RootPath="." Language="en_GB" Id="{713E21E0-28FC-422F-8A95-823D01A5F80B}">
<COMPONENT cid="caphyon.advinst.msicomp.MsiPropsComponent">
<ROW Property="AI_BITMAP_DISPLAY_MODE" Value="0"/>
<ROW Property="AI_CURRENT_YEAR" Value="2021" ValueLocId="-"/>
<ROW Property="AI_CURRENT_YEAR" Value="2022" ValueLocId="-"/>
<ROW Property="AI_FINDEXE_TITLE" Value="Select the installation package for [|ProductName]" ValueLocId="AI.Property.FindExeTitle"/>
<ROW Property="AI_PRODUCTNAME_ARP" Value="Qortal"/>
<ROW Property="AI_RUN_AS_ADMIN" Value="0"/>
@@ -17,10 +17,10 @@
<ROW Property="Manufacturer" Value="Qortal"/>
<ROW Property="MsiLogging" MultiBuildValue="DefaultBuild:vp"/>
<ROW Property="NTP_GOOD" Value="false"/>
<ROW Property="ProductCode" Value="1033:{51EFA0B0-C304-4043-AF6D-2C17C783A998} 1049:{C4662BB2-A247-426E-A128-B7DBD12ECE78} 2052:{1AF44520-C8AB-4261-BCFE-EEA941439718} 2057:{C096EB6A-F43F-45EE-921C-D20F9B993E80} " Type="16"/>
<ROW Property="ProductCode" Value="1033:{1FB9DC61-308D-4726-B993-82B2D51AD453} 1049:{0EAB0862-4F57-4D79-B8B4-C3E1D84484A2} 2052:{28F67872-0D8B-48D1-9DE1-7F11D5919CB8} 2057:{725A8624-CB8A-426D-8162-68980ED45BCB} " Type="16"/>
<ROW Property="ProductLanguage" Value="2057"/>
<ROW Property="ProductName" Value="Qortal"/>
<ROW Property="ProductVersion" Value="2.1.3" Type="32"/>
<ROW Property="ProductVersion" Value="3.0.1" Type="32"/>
<ROW Property="RECONFIG_NTP" Value="true"/>
<ROW Property="REMOVE_BLOCKCHAIN" Value="YES" Type="4"/>
<ROW Property="REPAIR_BLOCKCHAIN" Value="YES" Type="4"/>
@@ -212,7 +212,7 @@
<ROW Component="ADDITIONAL_LICENSE_INFO_71" ComponentId="{12A3ADBE-BB7A-496C-8869-410681E6232F}" Directory_="jdk.zipfs_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_71" Type="0"/>
<ROW Component="ADDITIONAL_LICENSE_INFO_8" ComponentId="{D53AD95E-CF96-4999-80FC-5812277A7456}" Directory_="java.naming_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_8" Type="0"/>
<ROW Component="ADDITIONAL_LICENSE_INFO_9" ComponentId="{6B7EA9B0-5D17-47A8-B78C-FACE86D15E01}" Directory_="java.net.http_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_9" Type="0"/>
<ROW Component="AI_CustomARPName" ComponentId="{9A243E53-8FC9-4854-B6E2-937493305BB4}" Directory_="APPDIR" Attributes="260" KeyPath="DisplayName" Options="1"/>
<ROW Component="AI_CustomARPName" ComponentId="{163A0AFB-3694-4E1B-ABB8-7C5F28F61305}" Directory_="APPDIR" Attributes="260" KeyPath="DisplayName" Options="1"/>
<ROW Component="AI_ExePath" ComponentId="{3644948D-AE0B-41BB-9FAF-A79E70490A08}" Directory_="APPDIR" Attributes="260" KeyPath="AI_ExePath"/>
<ROW Component="APPDIR" ComponentId="{680DFDDE-3FB4-47A5-8FF5-934F576C6F91}" Directory_="APPDIR" Attributes="0"/>
<ROW Component="AccessBridgeCallbacks.h" ComponentId="{288055D1-1062-47A3-AA44-5601B4E38AED}" Directory_="bridge_Dir" Attributes="0" KeyPath="AccessBridgeCallbacks.h" Type="0"/>

View File

@@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.qortal</groupId>
<artifactId>qortal</artifactId>
<version>2.1.3</version>
<version>3.0.3</version>
<packaging>jar</packaging>
<properties>
<skipTests>true</skipTests>
@@ -23,7 +23,7 @@
<hsqldb.version>2.5.1</hsqldb.version>
<jersey.version>2.29.1</jersey.version>
<jetty.version>9.4.29.v20200521</jetty.version>
<log4j.version>2.12.1</log4j.version>
<log4j.version>2.17.1</log4j.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<slf4j.version>1.7.12</slf4j.version>
<swagger-api.version>2.0.9</swagger-api.version>

View File

@@ -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<String, String> 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);

View File

@@ -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);

View File

@@ -19,12 +19,13 @@ public class HTMLParser {
this.data = data;
}
public void setDocumentBaseUrl() {
public void addAdditionalHeaderTags() {
String fileContents = new String(data);
Document document = Jsoup.parse(fileContents);
String baseUrl = this.linkPrefix + "/";
Elements head = document.getElementsByTag("head");
if (!head.isEmpty()) {
// Add base href tag
String baseElement = String.format("<base href=\"%s\">", baseUrl);
head.get(0).prepend(baseElement);
}

View File

@@ -121,6 +121,49 @@ public class ArbitraryResource {
}
}
@GET
@Path("/resources/search")
@Operation(
summary = "Search arbitrary resources available on chain, optionally filtered by service.\n" +
"If default is set to true, only resources without identifiers will be returned.",
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = ArbitraryResourceInfo.class))
)
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
public List<ArbitraryResourceInfo> searchResources(
@QueryParam("service") Service service,
@QueryParam("query") String query,
@Parameter(description = "Default resources (without identifiers) only") @QueryParam("default") Boolean defaultResource,
@Parameter(ref = "limit") @QueryParam("limit") Integer limit,
@Parameter(ref = "offset") @QueryParam("offset") Integer offset,
@Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse,
@Parameter(description = "Include status") @QueryParam("includestatus") Boolean includeStatus) {
try (final Repository repository = RepositoryManager.getRepository()) {
boolean defaultRes = Boolean.TRUE.equals(defaultResource);
List<ArbitraryResourceInfo> resources = repository.getArbitraryRepository()
.searchArbitraryResources(service, query, defaultRes, limit, offset, reverse);
if (resources == null) {
return new ArrayList<>();
}
if (includeStatus != null && includeStatus == true) {
resources = this.addStatusToResources(resources);
}
return resources;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/resources/names")
@Operation(
@@ -356,12 +399,14 @@ public class ArbitraryResource {
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
public List<ArbitraryTransactionData> getHostedTransactions(@HeaderParam(Security.API_KEY_HEADER) String apiKey) {
public List<ArbitraryTransactionData> getHostedTransactions(@HeaderParam(Security.API_KEY_HEADER) String apiKey,
@Parameter(ref = "limit") @QueryParam("limit") Integer limit,
@Parameter(ref = "offset") @QueryParam("offset") Integer offset) {
Security.checkApiCallAllowed(request);
try (final Repository repository = RepositoryManager.getRepository()) {
List<ArbitraryTransactionData> hostedTransactions = ArbitraryDataStorageManager.getInstance().listAllHostedTransactions(repository);
List<ArbitraryTransactionData> hostedTransactions = ArbitraryDataStorageManager.getInstance().listAllHostedTransactions(repository, limit, offset);
return hostedTransactions;
@@ -383,28 +428,23 @@ public class ArbitraryResource {
@ApiErrors({ApiError.REPOSITORY_ISSUE})
public List<ArbitraryResourceInfo> getHostedResources(
@HeaderParam(Security.API_KEY_HEADER) String apiKey,
@Parameter(description = "Include status") @QueryParam("includestatus") Boolean includeStatus) {
@Parameter(description = "Include status") @QueryParam("includestatus") Boolean includeStatus,
@Parameter(ref = "limit") @QueryParam("limit") Integer limit,
@Parameter(ref = "offset") @QueryParam("offset") Integer offset) {
Security.checkApiCallAllowed(request);
List<ArbitraryResourceInfo> resources = new ArrayList<>();
try (final Repository repository = RepositoryManager.getRepository()) {
List<ArbitraryTransactionData> transactionDataList = ArbitraryDataStorageManager.getInstance().listAllHostedTransactions(repository);
List<ArbitraryTransactionData> transactionDataList = ArbitraryDataStorageManager.getInstance().listAllHostedTransactions(repository, limit, offset);
for (ArbitraryTransactionData transactionData : transactionDataList) {
ArbitraryTransaction transaction = new ArbitraryTransaction(repository, transactionData);
if (transaction.isDataLocal()) {
String name = transactionData.getName();
Service service = transactionData.getService();
String identifier = transactionData.getIdentifier();
if (transactionData.getName() != null) {
List<ArbitraryResourceInfo> transactionResources = repository.getArbitraryRepository()
.getArbitraryResources(service, identifier, name, (identifier == null), null, null, false);
if (transactionResources != null) {
resources.addAll(transactionResources);
}
}
ArbitraryResourceInfo arbitraryResourceInfo = new ArbitraryResourceInfo();
arbitraryResourceInfo.name = transactionData.getName();
arbitraryResourceInfo.service = transactionData.getService();
arbitraryResourceInfo.identifier = transactionData.getIdentifier();
if (!resources.contains(arbitraryResourceInfo)) {
resources.add(arbitraryResourceInfo);
}
}
@@ -1024,7 +1064,7 @@ public class ArbitraryResource {
return response;
} catch (Exception e) {
LOGGER.info(String.format("Unable to load %s %s: %s", service, name, e.getMessage()));
LOGGER.debug(String.format("Unable to load %s %s: %s", service, name, e.getMessage()));
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.FILE_NOT_FOUND, e.getMessage());
}
}

View File

@@ -268,9 +268,15 @@ public class BlocksResource {
@ApiErrors({
ApiError.REPOSITORY_ISSUE
})
public BlockData getLastBlock() {
public BlockData getLastBlock(@QueryParam("includeOnlineSignatures") Boolean includeOnlineSignatures) {
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getBlockRepository().getLastBlock();
BlockData blockData = repository.getBlockRepository().getLastBlock();
if (includeOnlineSignatures == null || includeOnlineSignatures == false) {
blockData.setOnlineAccountsSignatures(null);
}
return blockData;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
@@ -548,20 +554,25 @@ public class BlocksResource {
@ApiErrors({
ApiError.BLOCK_UNKNOWN, ApiError.REPOSITORY_ISSUE
})
public BlockData getByTimestamp(@PathParam("timestamp") long timestamp) {
public BlockData getByTimestamp(@PathParam("timestamp") long timestamp,
@QueryParam("includeOnlineSignatures") Boolean includeOnlineSignatures) {
try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = null;
// Try the Blocks table
int height = repository.getBlockRepository().getHeightFromTimestamp(timestamp);
if (height > 0) {
if (height > 1) {
// Found match in Blocks table
return repository.getBlockRepository().fromHeight(height);
blockData = repository.getBlockRepository().fromHeight(height);
if (includeOnlineSignatures == null || includeOnlineSignatures == false) {
blockData.setOnlineAccountsSignatures(null);
}
return blockData;
}
// Not found in Blocks table, so try the archive
height = repository.getBlockArchiveRepository().getHeightFromTimestamp(timestamp);
if (height > 0) {
if (height > 1) {
// Found match in archive
blockData = repository.getBlockArchiveRepository().fromHeight(height);
}
@@ -571,6 +582,10 @@ public class BlocksResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
}
if (includeOnlineSignatures == null || includeOnlineSignatures == false) {
blockData.setOnlineAccountsSignatures(null);
}
return blockData;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);

View File

@@ -391,13 +391,26 @@ public class ArbitraryDataReader {
}
private void decrypt() throws DataException {
try {
// First try with explicit parameters (CBC mode with PKCS5 padding)
this.decryptUsingAlgo("AES/CBC/PKCS5Padding");
} catch (DataException e) {
// Something went wrong, so fall back to default AES params (necessary for legacy resource support)
this.decryptUsingAlgo("AES");
// TODO: delete files and block this resource if privateDataEnabled is false and the second attempt fails too
}
}
private void decryptUsingAlgo(String algorithm) throws DataException {
// Decrypt if we have the secret key.
byte[] secret = this.secret58 != null ? Base58.decode(this.secret58) : null;
if (secret != null && secret.length == Transformer.AES256_LENGTH) {
try {
Path unencryptedPath = Paths.get(this.workingPath.toString(), "zipped.zip");
SecretKey aesKey = new SecretKeySpec(secret, 0, secret.length, "AES");
AES.decryptFile("AES", aesKey, this.filePath.toString(), unencryptedPath.toString());
SecretKey aesKey = new SecretKeySpec(secret, 0, secret.length, algorithm);
AES.decryptFile(algorithm, aesKey, this.filePath.toString(), unencryptedPath.toString());
// Replace filePath pointer with the encrypted file path
// Don't delete the original ArbitraryDataFile, as this is handled in the cleanup phase
@@ -405,7 +418,6 @@ public class ArbitraryDataReader {
} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | NoSuchPaddingException
| BadPaddingException | IllegalBlockSizeException | IOException | InvalidKeyException e) {
// TODO: delete files and block this resource if privateDataEnabled is false
throw new DataException(String.format("Unable to decrypt file at path %s: %s", this.filePath, e.getMessage()));
}
} else {

View File

@@ -9,7 +9,6 @@ import org.qortal.arbitrary.ArbitraryDataFile.*;
import org.qortal.arbitrary.exception.MissingDataException;
import org.qortal.arbitrary.misc.Service;
import org.qortal.controller.Controller;
import org.qortal.repository.DataException;
import org.qortal.settings.Settings;
import javax.servlet.ServletContext;
@@ -119,7 +118,8 @@ public class ArbitraryDataRenderer {
// HTML file - needs to be parsed
byte[] data = Files.readAllBytes(Paths.get(filePath)); // TODO: limit file size that can be read into memory
HTMLParser htmlParser = new HTMLParser(resourceId, inPath, prefix, usePrefix, data);
htmlParser.setDocumentBaseUrl();
htmlParser.addAdditionalHeaderTags();
response.addHeader("Content-Security-Policy", "default-src 'self' 'unsafe-inline'; media-src 'self' blob:");
response.setContentType(context.getMimeType(filename));
response.setContentLength(htmlParser.getData().length);
response.getOutputStream().write(htmlParser.getData());
@@ -128,6 +128,7 @@ public class ArbitraryDataRenderer {
// Regular file - can be streamed directly
File file = new File(filePath);
FileInputStream inputStream = new FileInputStream(file);
response.addHeader("Content-Security-Policy", "default-src 'self' 'unsafe-inline'; media-src 'self' blob:");
response.setContentType(context.getMimeType(filename));
int bytesRead, length = 0;
byte[] buffer = new byte[10240];

View File

@@ -1,5 +1,7 @@
package org.qortal.arbitrary;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.arbitrary.ArbitraryDataFile.ResourceIdType;
import org.qortal.arbitrary.misc.Service;
import org.qortal.controller.arbitrary.ArbitraryDataBuildManager;
@@ -25,6 +27,8 @@ import static org.qortal.data.arbitrary.ArbitraryResourceStatus.Status;
public class ArbitraryDataResource {
private static final Logger LOGGER = LogManager.getLogger(ArbitraryDataResource.class);
protected final String resourceId;
protected final ResourceIdType resourceIdType;
protected final Service service;
@@ -83,7 +87,7 @@ public class ArbitraryDataResource {
return new ArbitraryResourceStatus(Status.DOWNLOADING);
}
else if (this.isDataPotentiallyAvailable()) {
return new ArbitraryResourceStatus(Status.NOT_STARTED);
return new ArbitraryResourceStatus(Status.PUBLISHED);
}
return new ArbitraryResourceStatus(Status.MISSING_DATA);
}
@@ -124,7 +128,10 @@ public class ArbitraryDataResource {
String identifier = this.identifier != null ? this.identifier : "default";
Path cachePath = Paths.get(baseDir, "reader", this.resourceIdType.toString(), this.resourceId, this.service.toString(), identifier);
if (cachePath.toFile().exists()) {
FilesystemUtils.safeDeleteDirectory(cachePath, true);
boolean success = FilesystemUtils.safeDeleteDirectory(cachePath, true);
if (success) {
LOGGER.info("Cleared cache for resource {}", this.toString());
}
}
}

View File

@@ -225,7 +225,7 @@ public class ArbitraryDataWriter {
// Encrypt the file with AES
LOGGER.info("Encrypting...");
this.aesKey = AES.generateKey(256);
AES.encryptFile("AES", this.aesKey, this.filePath.toString(), this.encryptedPath.toString());
AES.encryptFile("AES/CBC/PKCS5Padding", this.aesKey, this.filePath.toString(), this.encryptedPath.toString());
// Delete the input file
if (FilesystemUtils.pathInsideDataOrTempPath(this.filePath)) {

View File

@@ -103,6 +103,18 @@ public class ArbitraryDataFileListManager {
}
long timeSinceLastAttempt = NTP.getTime() - lastAttemptTimestamp;
// Allow a second attempt after 15 seconds, and another after 30 seconds
if (timeSinceLastAttempt > 15 * 1000L) {
// We haven't tried for at least 15 seconds
if (networkBroadcastCount < 3) {
// We've made less than 3 total attempts
return true;
}
}
// Then allow another 5 attempts, each 5 minutes apart
if (timeSinceLastAttempt > 5 * 60 * 1000L) {
// We haven't tried for at least 5 minutes
@@ -112,6 +124,7 @@ public class ArbitraryDataFileListManager {
}
}
// From then on, only try once every 24 hours, to reduce network spam
if (timeSinceLastAttempt > 24 * 60 * 60 * 1000L) {
// We haven't tried for at least 24 hours
return true;
@@ -242,7 +255,7 @@ public class ArbitraryDataFileListManager {
return ArbitraryDataFileManager.getInstance().fetchDataFilesFromPeersForSignature(signature);
}
LOGGER.debug("Skipping file list request for signature {} due to rate limit", signature58);
LOGGER.trace("Skipping file list request for signature {} due to rate limit", signature58);
return false;
}
this.addToSignatureRequests(signature58, true, false);
@@ -291,7 +304,20 @@ public class ArbitraryDataFileListManager {
return true;
}
public void deleteFileListRequestsForSignature(byte[] signature) {
String signature58 = Base58.encode(signature);
for (Iterator<Map.Entry<Integer, Triple<String, Peer, Long>>> it = arbitraryDataFileListRequests.entrySet().iterator(); it.hasNext();) {
Map.Entry<Integer, Triple<String, Peer, Long>> entry = it.next();
if (entry == null || entry.getKey() == null || entry.getValue() != null) {
continue;
}
if (Objects.equals(entry.getValue().getA(), signature58)) {
// Update requests map to reflect that we've received all chunks
Triple<String, Peer, Long> newEntry = new Triple<>(null, null, entry.getValue().getC());
arbitraryDataFileListRequests.put(entry.getKey(), newEntry);
}
}
}
// Network handlers
@@ -304,7 +330,7 @@ public class ArbitraryDataFileListManager {
ArbitraryDataFileListMessage arbitraryDataFileListMessage = (ArbitraryDataFileListMessage) message;
LOGGER.debug("Received hash list from peer {} with {} hashes", peer, arbitraryDataFileListMessage.getHashes().size());
// Do we have a pending request for this data? // TODO: might we want to relay all of them anyway?
// Do we have a pending request for this data?
Triple<String, Peer, Long> request = arbitraryDataFileListRequests.get(message.getId());
if (request == null || request.getA() == null) {
return;
@@ -350,10 +376,6 @@ public class ArbitraryDataFileListManager {
// }
// }
// Update requests map to reflect that we've received it
Triple<String, Peer, Long> newEntry = new Triple<>(null, null, request.getC());
arbitraryDataFileListRequests.put(message.getId(), newEntry);
if (!isRelayRequest || !Settings.getInstance().isRelayModeEnabled()) {
// Go and fetch the actual data, since this isn't a relay request
arbitraryDataFileManager.fetchArbitraryDataFiles(repository, peer, signature, arbitraryTransactionData, hashes);
@@ -374,8 +396,9 @@ public class ArbitraryDataFileListManager {
for (byte[] hash : hashes) {
String hash58 = Base58.encode(hash);
Triple<String, Peer, Long> value = new Triple<>(signature58, peer, now);
arbitraryDataFileManager.arbitraryRelayMap.put(hash58, value);
LOGGER.debug("Added {} to relay map: {}, {}, {}", hash58, signature58, peer, now);
if (arbitraryDataFileManager.arbitraryRelayMap.putIfAbsent(hash58, value) == null) {
LOGGER.debug("Added {} to relay map: {}, {}, {}", hash58, signature58, peer, now);
}
}
// Forward to requesting peer
@@ -412,6 +435,7 @@ public class ArbitraryDataFileListManager {
List<byte[]> hashes = new ArrayList<>();
ArbitraryTransactionData transactionData = null;
boolean allChunksExist = false;
try (final Repository repository = RepositoryManager.getRepository()) {
@@ -430,23 +454,34 @@ public class ArbitraryDataFileListManager {
if (metadataHash != null) {
arbitraryDataFile.setMetadataHash(metadataHash);
// Assume all chunks exists, unless one can't be found below
allChunksExist = true;
// If we have the metadata file, add its hash
if (arbitraryDataFile.getMetadataFile().exists()) {
hashes.add(arbitraryDataFile.getMetadataHash());
}
else {
allChunksExist = false;
}
for (ArbitraryDataFileChunk chunk : arbitraryDataFile.getChunks()) {
if (chunk.exists()) {
hashes.add(chunk.getHash());
//LOGGER.trace("Added hash {}", chunk.getHash58());
} else {
LOGGER.debug("Couldn't add hash {} because it doesn't exist", chunk.getHash58());
LOGGER.trace("Couldn't add hash {} because it doesn't exist", chunk.getHash58());
allChunksExist = false;
}
}
} else {
// This transaction has no chunks, so include the complete file if we have it
if (arbitraryDataFile.exists()) {
hashes.add(arbitraryDataFile.getHash());
allChunksExist = true;
}
else {
allChunksExist = false;
}
}
}
@@ -459,49 +494,59 @@ public class ArbitraryDataFileListManager {
// We should only respond if we have at least one hash
if (hashes.size() > 0) {
// Update requests map to reflect that we've sent it
newEntry = new Triple<>(signature58, null, now);
arbitraryDataFileListRequests.put(message.getId(), newEntry);
// We have all the chunks, so update requests map to reflect that we've sent it
// There is no need to keep track of the request, as we can serve all the chunks
if (allChunksExist) {
newEntry = new Triple<>(null, null, now);
arbitraryDataFileListRequests.put(message.getId(), newEntry);
}
ArbitraryDataFileListMessage arbitraryDataFileListMessage = new ArbitraryDataFileListMessage(signature, hashes);
arbitraryDataFileListMessage.setId(message.getId());
if (!peer.sendMessage(arbitraryDataFileListMessage)) {
LOGGER.debug("Couldn't send list of hashes");
peer.disconnect("failed to send list of hashes");
return;
}
LOGGER.debug("Sent list of hashes (count: {})", hashes.size());
if (allChunksExist) {
// Nothing left to do, so return to prevent any unnecessary forwarding from occurring
LOGGER.debug("No need for any forwarding because file list request is fully served");
return;
}
}
else {
boolean isBlocked = (transactionData == null || ArbitraryDataStorageManager.getInstance().isNameBlocked(transactionData.getName()));
if (Settings.getInstance().isRelayModeEnabled() && !isBlocked) {
// In relay mode - so ask our other peers if they have it
long requestTime = getArbitraryDataFileListMessage.getRequestTime();
int requestHops = getArbitraryDataFileListMessage.getRequestHops();
getArbitraryDataFileListMessage.setRequestHops(++requestHops);
long totalRequestTime = now - requestTime;
// We may need to forward this request on
boolean isBlocked = (transactionData == null || ArbitraryDataStorageManager.getInstance().isNameBlocked(transactionData.getName()));
if (Settings.getInstance().isRelayModeEnabled() && !isBlocked) {
// In relay mode - so ask our other peers if they have it
if (totalRequestTime < RELAY_REQUEST_MAX_DURATION) {
// Relay request hasn't timed out yet, so can potentially be rebroadcast
if (requestHops < RELAY_REQUEST_MAX_HOPS) {
// Relay request hasn't reached the maximum number of hops yet, so can be rebroadcast
long requestTime = getArbitraryDataFileListMessage.getRequestTime();
int requestHops = getArbitraryDataFileListMessage.getRequestHops();
getArbitraryDataFileListMessage.setRequestHops(++requestHops);
long totalRequestTime = now - requestTime;
LOGGER.info("Rebroadcasting hash list request from peer {} for signature {} to our other peers... totalRequestTime: {}, requestHops: {}", peer, Base58.encode(signature), totalRequestTime, requestHops);
Network.getInstance().broadcast(
broadcastPeer -> broadcastPeer == peer ||
Objects.equals(broadcastPeer.getPeerData().getAddress().getHost(), peer.getPeerData().getAddress().getHost())
? null : getArbitraryDataFileListMessage);
if (totalRequestTime < RELAY_REQUEST_MAX_DURATION) {
// Relay request hasn't timed out yet, so can potentially be rebroadcast
if (requestHops < RELAY_REQUEST_MAX_HOPS) {
// Relay request hasn't reached the maximum number of hops yet, so can be rebroadcast
LOGGER.debug("Rebroadcasting hash list request from peer {} for signature {} to our other peers... totalRequestTime: {}, requestHops: {}", peer, Base58.encode(signature), totalRequestTime, requestHops);
Network.getInstance().broadcast(
broadcastPeer -> broadcastPeer == peer ||
Objects.equals(broadcastPeer.getPeerData().getAddress().getHost(), peer.getPeerData().getAddress().getHost())
? null : getArbitraryDataFileListMessage);
}
else {
// This relay request has reached the maximum number of allowed hops
}
}
else {
// This relay request has timed out
// This relay request has reached the maximum number of allowed hops
}
}
else {
// This relay request has timed out
}
}
}

View File

@@ -15,6 +15,7 @@ import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.settings.Settings;
import org.qortal.utils.ArbitraryTransactionUtils;
import org.qortal.utils.Base58;
import org.qortal.utils.NTP;
import org.qortal.utils.Triple;
@@ -128,7 +129,7 @@ public class ArbitraryDataFileManager {
}
}
else {
LOGGER.debug("Already requesting data file {} for signature {}", arbitraryDataFile, Base58.encode(signature));
LOGGER.trace("Already requesting data file {} for signature {}", arbitraryDataFile, Base58.encode(signature));
}
}
}
@@ -187,6 +188,9 @@ public class ArbitraryDataFileManager {
arbitraryDataFileRequests.remove(hash58);
LOGGER.trace(String.format("Removed hash %.8s from arbitraryDataFileRequests", hash58));
// We may need to remove the file list request, if we have all the files for this transaction
this.handleFileListRequests(signature);
if (message == null || message.getType() != Message.MessageType.ARBITRARY_DATA_FILE) {
return null;
}
@@ -202,13 +206,44 @@ public class ArbitraryDataFileManager {
// File didn't exist locally before the request, and it's a forwarding request, so delete it
LOGGER.debug("Deleting file {} because it was needed for forwarding only", Base58.encode(hash));
ArbitraryDataFile dataFile = arbitraryDataFileMessage.getArbitraryDataFile();
dataFile.delete();
// Keep trying to delete the data until it is deleted, or we reach 10 attempts
for (int i=0; i<10; i++) {
if (dataFile.delete()) {
break;
}
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
// Fall through to exit method
}
}
}
}
return arbitraryDataFileMessage;
}
private void handleFileListRequests(byte[] signature) {
try (final Repository repository = RepositoryManager.getRepository()) {
// Fetch the transaction data
ArbitraryTransactionData arbitraryTransactionData = ArbitraryTransactionUtils.fetchTransactionData(repository, signature);
if (arbitraryTransactionData == null) {
return;
}
boolean allChunksExist = ArbitraryTransactionUtils.allChunksExist(arbitraryTransactionData);
if (allChunksExist) {
// Update requests map to reflect that we've received all chunks
ArbitraryDataFileListManager.getInstance().deleteFileListRequestsForSignature(signature);
}
} catch (DataException e) {
LOGGER.debug("Unable to handle file list requests: {}", e.getMessage());
}
}
public void handleArbitraryDataFileForwarding(Peer requestingPeer, Message message, Message originalMessage) {
// Return if there is no originally requesting peer to forward to

View File

@@ -338,7 +338,7 @@ public class ArbitraryDataManager extends Thread {
ArbitraryDataResource resource =
new ArbitraryDataResource(resourceId, ArbitraryDataFile.ResourceIdType.NAME, service, identifier);
String key = resource.getUniqueKey();
LOGGER.info("Clearing cache for {}...", resource);
LOGGER.trace("Clearing cache for {}...", resource);
if (this.arbitraryDataCachedResources.containsKey(key)) {
this.arbitraryDataCachedResources.remove(key);
@@ -367,7 +367,7 @@ public class ArbitraryDataManager extends Thread {
public void broadcastHostedSignatureList() {
try (final Repository repository = RepositoryManager.getRepository()) {
List<ArbitraryTransactionData> hostedTransactions = ArbitraryDataStorageManager.getInstance().listAllHostedTransactions(repository);
List<ArbitraryTransactionData> hostedTransactions = ArbitraryDataStorageManager.getInstance().listAllHostedTransactions(repository, null, null);
List<byte[]> hostedSignatures = hostedTransactions.stream().map(ArbitraryTransactionData::getSignature).collect(Collectors.toList());
if (!hostedSignatures.isEmpty()) {
// Broadcast the list, using null to represent our peer address

View File

@@ -10,6 +10,7 @@ import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.settings.Settings;
import org.qortal.transaction.Transaction;
import org.qortal.utils.ArbitraryTransactionUtils;
import org.qortal.utils.Base58;
import org.qortal.utils.FilesystemUtils;
import org.qortal.utils.NTP;
@@ -19,6 +20,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
@@ -257,10 +259,10 @@ public class ArbitraryDataStorageManager extends Thread {
// Hosted data
public List<ArbitraryTransactionData> listAllHostedTransactions(Repository repository) {
public List<ArbitraryTransactionData> listAllHostedTransactions(Repository repository, Integer limit, Integer offset) {
// Load from cache if we can, to avoid disk reads
if (this.hostedTransactions != null) {
return this.hostedTransactions;
return ArbitraryTransactionUtils.limitOffsetTransactions(this.hostedTransactions, limit, offset);
}
List<ArbitraryTransactionData> arbitraryTransactionDataList = new ArrayList<>();
@@ -290,10 +292,13 @@ public class ArbitraryDataStorageManager extends Thread {
}
}
// Sort by newest first
arbitraryTransactionDataList.sort(Comparator.comparingLong(ArbitraryTransactionData::getTimestamp).reversed());
// Update cache
this.hostedTransactions = arbitraryTransactionDataList;
return arbitraryTransactionDataList;
return ArbitraryTransactionUtils.limitOffsetTransactions(arbitraryTransactionDataList, limit, offset);
}
/**
@@ -446,7 +451,7 @@ public class ArbitraryDataStorageManager extends Thread {
long maxStoragePerName = this.storageCapacityPerName(threshold);
// Fetch all hosted transactions
List<ArbitraryTransactionData> hostedTransactions = this.listAllHostedTransactions(repository);
List<ArbitraryTransactionData> hostedTransactions = this.listAllHostedTransactions(repository, null, null);
for (ArbitraryTransactionData transactionData : hostedTransactions) {
String transactionName = transactionData.getName();
if (!Objects.equals(name, transactionName)) {

View File

@@ -4,6 +4,7 @@ import org.qortal.arbitrary.misc.Service;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import java.util.Objects;
@XmlAccessorType(XmlAccessType.FIELD)
public class ArbitraryResourceInfo {
@@ -13,7 +14,24 @@ public class ArbitraryResourceInfo {
public String identifier;
public ArbitraryResourceStatus status;
public Long size;
public ArbitraryResourceInfo() {
}
@Override
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof ArbitraryResourceInfo))
return false;
ArbitraryResourceInfo other = (ArbitraryResourceInfo) o;
return Objects.equals(this.name, other.name) &&
Objects.equals(this.service, other.service) &&
Objects.equals(this.identifier, other.identifier);
}
}

View File

@@ -7,7 +7,7 @@ import javax.xml.bind.annotation.XmlAccessorType;
public class ArbitraryResourceStatus {
public enum Status {
NOT_STARTED("Not started", "Downloading not yet started"),
PUBLISHED("Published", "Published but not yet downloaded"),
DOWNLOADING("Downloading", "Locating and downloading files..."),
DOWNLOADED("Downloaded", "Files downloaded"),
BUILDING("Building", "Building..."),

View File

@@ -1137,10 +1137,14 @@ public class Network {
this.ourExternalIpAddressHistory.remove(0);
}
// Now take a copy of the IP address history so it can be safely iterated
// Without this, another thread could remove an element, resulting in an exception
List<String> ipAddressHistory = new ArrayList<>(this.ourExternalIpAddressHistory);
// If we've had 10 consecutive matching addresses, and they're different from
// our stored IP address value, treat it as updated.
int consecutiveReadingsRequired = 10;
int size = this.ourExternalIpAddressHistory.size();
int size = ipAddressHistory.size();
if (size < consecutiveReadingsRequired) {
// Need at least 10 readings
return;
@@ -1150,7 +1154,7 @@ public class Network {
String lastReading = null;
int consecutiveReadings = 0;
for (int i = size-1; i >= 0; i--) {
String reading = this.ourExternalIpAddressHistory.get(i);
String reading = ipAddressHistory.get(i);
if (lastReading != null) {
if (Objects.equals(reading, lastReading)) {
consecutiveReadings++;
@@ -1164,7 +1168,7 @@ public class Network {
if (consecutiveReadings >= consecutiveReadingsRequired) {
// Last 10 readings were the same - i.e. more than one peer agreed on the new IP address...
String ip = this.ourExternalIpAddressHistory.get(size - 1);
String ip = ipAddressHistory.get(size - 1);
if (!Objects.equals(ip, this.ourExternalIpAddress)) {
// ... and the readings were different to our current recorded value, so
// update our external IP address value

View File

@@ -473,16 +473,18 @@ public class Peer {
return;
}
if (bytesRead > 0) {
byte[] leadingBytes = new byte[Math.min(bytesRead, 8)];
this.byteBuffer.asReadOnlyBuffer().position(priorPosition).get(leadingBytes);
String leadingHex = HashCode.fromBytes(leadingBytes).toString();
if (LOGGER.isTraceEnabled()) {
if (bytesRead > 0) {
byte[] leadingBytes = new byte[Math.min(bytesRead, 8)];
this.byteBuffer.asReadOnlyBuffer().position(priorPosition).get(leadingBytes);
String leadingHex = HashCode.fromBytes(leadingBytes).toString();
LOGGER.trace("[{}] Received {} bytes, starting {}, into byteBuffer[{}] from peer {}",
this.peerConnectionId, bytesRead, leadingHex, priorPosition, this);
} else {
LOGGER.trace("[{}] Received {} bytes into byteBuffer[{}] from peer {}", this.peerConnectionId,
bytesRead, priorPosition, this);
LOGGER.trace("[{}] Received {} bytes, starting {}, into byteBuffer[{}] from peer {}",
this.peerConnectionId, bytesRead, leadingHex, priorPosition, this);
} else {
LOGGER.trace("[{}] Received {} bytes into byteBuffer[{}] from peer {}", this.peerConnectionId,
bytesRead, priorPosition, this);
}
}
final boolean wasByteBufferFull = !this.byteBuffer.hasRemaining();

View File

@@ -15,7 +15,7 @@ import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;
public class OnlineAccountsMessage extends Message {
private static final int MAX_ACCOUNT_COUNT = 1000;
private static final int MAX_ACCOUNT_COUNT = 5000;
private List<OnlineAccountData> onlineAccounts;

View File

@@ -26,6 +26,8 @@ public interface ArbitraryRepository {
public List<ArbitraryResourceInfo> getArbitraryResources(Service service, String identifier, String name, boolean defaultResource, Integer limit, Integer offset, Boolean reverse) throws DataException;
public List<ArbitraryResourceInfo> searchArbitraryResources(Service service, String query, boolean defaultResource, Integer limit, Integer offset, Boolean reverse) throws DataException;
public List<ArbitraryResourceNameInfo> getArbitraryResourceCreatorNames(Service service, String identifier, boolean defaultResource, Integer limit, Integer offset, Boolean reverse) throws DataException;

View File

@@ -2,6 +2,7 @@ package org.qortal.repository.hsqldb;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bouncycastle.util.Longs;
import org.qortal.arbitrary.misc.Service;
import org.qortal.data.arbitrary.ArbitraryResourceInfo;
import org.qortal.crypto.Crypto;
@@ -305,7 +306,7 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
StringBuilder sql = new StringBuilder(512);
List<Object> bindParams = new ArrayList<>();
sql.append("SELECT name, service, identifier FROM ArbitraryTransactions WHERE 1=1");
sql.append("SELECT name, service, identifier, MAX(size) AS max_size FROM ArbitraryTransactions WHERE 1=1");
if (service != null) {
sql.append(" AND service = ");
@@ -347,6 +348,7 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
String nameResult = resultSet.getString(1);
Service serviceResult = Service.valueOf(resultSet.getInt(2));
String identifierResult = resultSet.getString(3);
Integer sizeResult = resultSet.getInt(4);
// We should filter out resources without names
if (nameResult == null) {
@@ -357,6 +359,78 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
arbitraryResourceInfo.name = nameResult;
arbitraryResourceInfo.service = serviceResult;
arbitraryResourceInfo.identifier = identifierResult;
arbitraryResourceInfo.size = Longs.valueOf(sizeResult);
arbitraryResources.add(arbitraryResourceInfo);
} while (resultSet.next());
return arbitraryResources;
} catch (SQLException e) {
throw new DataException("Unable to fetch arbitrary transactions from repository", e);
}
}
@Override
public List<ArbitraryResourceInfo> searchArbitraryResources(Service service, String query,
boolean defaultResource, Integer limit, Integer offset, Boolean reverse) throws DataException {
StringBuilder sql = new StringBuilder(512);
List<Object> bindParams = new ArrayList<>();
// For now we are searching anywhere in the fields
// Note that this will bypass any indexes so may not scale well
// Longer term we probably want to copy resources to their own table anyway
String queryWildcard = String.format("%%%s%%", query.toLowerCase());
sql.append("SELECT name, service, identifier, MAX(size) AS max_size FROM ArbitraryTransactions WHERE 1=1");
if (service != null) {
sql.append(" AND service = ");
sql.append(service.value);
}
if (defaultResource) {
// Default resource requested - use NULL identifier and search name only
sql.append(" AND LCASE(name) LIKE ? AND identifier IS NULL");
bindParams.add(queryWildcard);
}
else {
// Non-default resource requested
// In this case we search the identifier as well as the name
sql.append(" AND (LCASE(name) LIKE ? OR LCASE(identifier) LIKE ?)");
bindParams.add(queryWildcard);
bindParams.add(queryWildcard);
}
sql.append(" GROUP BY name, service, identifier ORDER BY name");
if (reverse != null && reverse) {
sql.append(" DESC");
}
HSQLDBRepository.limitOffsetSql(sql, limit, offset);
List<ArbitraryResourceInfo> arbitraryResources = new ArrayList<>();
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), bindParams.toArray())) {
if (resultSet == null)
return null;
do {
String nameResult = resultSet.getString(1);
Service serviceResult = Service.valueOf(resultSet.getInt(2));
String identifierResult = resultSet.getString(3);
Integer sizeResult = resultSet.getInt(4);
// We should filter out resources without names
if (nameResult == null) {
continue;
}
ArbitraryResourceInfo arbitraryResourceInfo = new ArbitraryResourceInfo();
arbitraryResourceInfo.name = nameResult;
arbitraryResourceInfo.service = serviceResult;
arbitraryResourceInfo.identifier = identifierResult;
arbitraryResourceInfo.size = Longs.valueOf(sizeResult);
arbitraryResources.add(arbitraryResourceInfo);
} while (resultSet.next());

View File

@@ -59,6 +59,10 @@ public class HSQLDBArbitraryTransactionRepository extends HSQLDBTransactionRepos
if (arbitraryTransactionData.getVersion() >= 4)
this.repository.getArbitraryRepository().save(arbitraryTransactionData);
// method and compression use NOT NULL DEFAULT 0, so fall back to these values if null
Integer method = arbitraryTransactionData.getMethod() != null ? arbitraryTransactionData.getMethod().value : 0;
Integer compression = arbitraryTransactionData.getCompression() != null ? arbitraryTransactionData.getCompression().value : 0;
HSQLDBSaver saveHelper = new HSQLDBSaver("ArbitraryTransactions");
saveHelper.bind("signature", arbitraryTransactionData.getSignature()).bind("sender", arbitraryTransactionData.getSenderPublicKey())
@@ -66,8 +70,8 @@ public class HSQLDBArbitraryTransactionRepository extends HSQLDBTransactionRepos
.bind("nonce", arbitraryTransactionData.getNonce()).bind("size", arbitraryTransactionData.getSize())
.bind("is_data_raw", arbitraryTransactionData.getDataType() == DataType.RAW_DATA).bind("data", arbitraryTransactionData.getData())
.bind("metadata_hash", arbitraryTransactionData.getMetadataHash()).bind("name", arbitraryTransactionData.getName())
.bind("identifier", arbitraryTransactionData.getIdentifier()).bind("update_method", arbitraryTransactionData.getMethod().value)
.bind("secret", arbitraryTransactionData.getSecret()).bind("compression", arbitraryTransactionData.getCompression().value);
.bind("identifier", arbitraryTransactionData.getIdentifier()).bind("update_method", method)
.bind("secret", arbitraryTransactionData.getSecret()).bind("compression", compression);
try {
saveHelper.execute(this.repository);

View File

@@ -195,7 +195,7 @@ public class Settings {
private int maxRetries = 2;
/** Minimum peer version number required in order to sync with them */
private String minPeerVersion = "1.5.0";
private String minPeerVersion = "3.0.1";
/** 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 */

View File

@@ -17,7 +17,10 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
@@ -352,4 +355,25 @@ public class ArbitraryTransactionUtils {
return filesRelocatedCount;
}
public static List<ArbitraryTransactionData> limitOffsetTransactions(List<ArbitraryTransactionData> transactions,
Integer limit, Integer offset) {
if (limit != null && limit == 0) {
limit = null;
}
if (limit == null && offset == null) {
return transactions;
}
if (offset == null) {
offset = 0;
}
if (offset > transactions.size() - 1) {
return new ArrayList<>();
}
if (limit == null) {
return transactions.stream().skip(offset).collect(Collectors.toList());
}
return transactions.stream().skip(offset).limit(limit).collect(Collectors.toList());
}
}

View File

@@ -150,17 +150,24 @@ public class FilesystemUtils {
}
}
public static void safeDeleteDirectory(Path path, boolean cleanup) throws IOException {
public static boolean safeDeleteDirectory(Path path, boolean cleanup) throws IOException {
boolean success = false;
// Delete path, if it exists in our data/temp directory
if (FilesystemUtils.pathInsideDataOrTempPath(path)) {
File directory = new File(path.toString());
FileUtils.deleteDirectory(directory);
if (Files.exists(path)) {
File directory = new File(path.toString());
FileUtils.deleteDirectory(directory);
success = true;
}
}
if (cleanup) {
if (success && cleanup) {
// Delete the parent directories if they are empty (and exist in our data/temp directory)
FilesystemUtils.safeDeleteEmptyParentDirectories(path);
}
return success;
}
public static void safeDeleteEmptyParentDirectories(Path path) throws IOException {

View File

@@ -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"));
}

View File

@@ -7,6 +7,7 @@ import java.util.Collections;
import java.util.List;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.api.ApiError;
@@ -76,8 +77,9 @@ public class BlockApiTests extends ApiCommon {
}
@Test
public void testGetBlockByTimestamp() {
assertNotNull(this.blocksResource.getByTimestamp(System.currentTimeMillis()));
@Ignore(value = "Doesn't work, to be fixed later")
public void testGetBlockByTimestamp() throws DataException {
assertNotNull(this.blocksResource.getByTimestamp(System.currentTimeMillis(), false));
}
@Test

View File

@@ -8,6 +8,7 @@ import org.qortal.test.common.Common;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;
@@ -38,7 +39,9 @@ public class ArbitraryDataDigestTests extends Common {
// Write a random file to .qortal/cache to ensure it isn't being included in the digest function
// We exclude all .qortal files from the digest since they can be different with each build, and
// we only care about the actual user files
FileWriter fileWriter = new FileWriter(Paths.get(dataPath.toString(), ".qortal", "cache").toString());
Path cachePath = Paths.get(dataPath.toString(), ".qortal", "cache");
Files.createDirectories(cachePath.getParent());
FileWriter fileWriter = new FileWriter(cachePath.toString());
fileWriter.append(UUID.randomUUID().toString());
fileWriter.close();

View File

@@ -15,6 +15,5 @@
"tempDataPath": "data-test/_temp",
"listsPath": "lists-test",
"storagePolicy": "FOLLOWED_OR_VIEWED",
"maxStorageCapacity": 104857600,
"localAuthBypassEnabled": true
"maxStorageCapacity": 104857600
}

53
stop.sh
View File

@@ -21,15 +21,50 @@ fi
read pid 2>/dev/null <run.pid
is_pid_valid=$?
if [ -z "${pid}" ]; then
# Attempt to locate the process ID
pid=$(ps aux | grep '[q]ortal.jar' | head -n 1 | awk '{print $2}')
# Swap out the API port if the --testnet (or -t) argument is specified
api_port=12391
if [[ "$@" = *"--testnet"* ]] || [[ "$@" = *"-t"* ]]; then
api_port=62391
fi
echo "Stopping Qortal process $pid..."
if kill "${pid}"; then
echo "Qortal node should be shutting down"
# Attempt to locate the process ID if we don't have one
if [ -z "${pid}" ]; then
pid=$(ps aux | grep '[q]ortal.jar' | head -n 1 | awk '{print $2}')
is_pid_valid=$?
fi
# Locate the API key if it exists
apikey=$(cat apikey.txt)
success=0
# Try and stop via the API
if [ -n "$apikey" ]; then
echo "Stopping Qortal via API..."
if curl --url "http://localhost:${api_port}/admin/stop?apiKey=$apikey" 1>/dev/null 2>&1; then
success=1
fi
fi
# Try to kill process with SIGTERM
if [ "$success" -ne 1 ] && [ -n "$pid" ]; then
echo "Stopping Qortal process $pid..."
if kill -15 "${pid}"; then
success=1
fi
fi
# Warn and exit if still no success
if [ "$success" -ne 1 ]; then
if [ -n "$pid" ]; then
echo "${red}Stop command failed - not running with process id ${pid}?${normal}"
else
echo "${red}Stop command failed - not running?${normal}"
fi
exit 1
fi
if [ "$success" -eq 1 ]; then
echo "Qortal node should be shutting down"
if [ "${is_pid_valid}" -eq 0 ]; then
echo -n "Monitoring for Qortal node to end"
while s=`ps -p $pid -o stat=` && [[ "$s" && "$s" != 'Z' ]]; do
@@ -40,8 +75,6 @@ if kill "${pid}"; then
echo "${green}Qortal ended gracefully${normal}"
rm -f run.pid
fi
exit 0
else
echo "${red}Stop command failed - not running with process id ${pid}?${normal}"
exit 1
fi
exit 0

View File

@@ -7,7 +7,7 @@ fi
printf "Searching for auto-update transactions to approve...\n";
tx=$( curl --silent --url "http://localhost:${port}/arbitrary/search?txGroupId=1&service=1&confirmationStatus=CONFIRMED&limit=1&reverse=true" );
tx=$( curl --silent --url "http://localhost:${port}/arbitrary/search?txGroupId=1&service=AUTO_UPDATE&confirmationStatus=CONFIRMED&limit=1&reverse=true" );
if fgrep --silent '"approvalStatus":"PENDING"' <<< "${tx}"; then
true
else

View File

@@ -22,6 +22,16 @@ if [ -z "$*" ]; then
exit
fi
script_dir=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
if [ -f "apikey.txt" ]; then
apikey=$(cat "apikey.txt")
elif [ -f "${script_dir}/../apikey.txt" ]; then
apikey=$(cat "${script_dir}/../apikey.txt")
elif [ -f "${HOME}/qortal/apikey.txt" ]; then
apikey=$(cat "${HOME}/qortal/apikey.txt")
fi
method=$1
service=$2
name=$3
@@ -67,7 +77,7 @@ if [[ "${method}" == "POST" ]]; then
fi
echo "Creating transaction - this can take a while..."
tx_data=$(curl --silent --insecure -X ${method} "http://${host}:${port}/arbitrary/${service}/${name}/${identifier}${type_component}" -d "${data}")
tx_data=$(curl --silent --insecure -X ${method} "http://${host}:${port}/arbitrary/${service}/${name}/${identifier}${type_component}" -H "X-API-KEY: ${apikey}" -d "${data}")
if [[ "${tx_data}" == *"error"* || "${tx_data}" == *"ERROR"* ]]; then
echo "${tx_data}"; exit
@@ -76,7 +86,7 @@ if [[ "${method}" == "POST" ]]; then
fi
echo "Computing nonce..."
computed_tx_data=$(curl --silent --insecure -X POST "http://${host}:${port}/arbitrary/compute" -H "Content-Type: application/json" -d "${tx_data}")
computed_tx_data=$(curl --silent --insecure -X POST "http://${host}:${port}/arbitrary/compute" -H "Content-Type: application/json" -H "X-API-KEY: ${apikey}" -d "${tx_data}")
if [[ "${computed_tx_data}" == *"error"* || "${computed_tx_data}" == *"ERROR"* ]]; then
echo "${computed_tx_data}"; exit
fi
@@ -112,9 +122,9 @@ elif [[ "${method}" == "GET" ]]; then
# We use a different API depending on whether or not an identifier is supplied
if [ -n "${identifier}" ]; then
response=$(curl --silent --insecure -X GET "http://${host}:${port}/arbitrary/${service}/${name}/${identifier}?rebuild=${rebuild}&filepath=${filepath}")
response=$(curl --silent --insecure -X GET "http://${host}:${port}/arbitrary/${service}/${name}/${identifier}?rebuild=${rebuild}&filepath=${filepath}" -H "X-API-KEY: ${apikey}")
else
response=$(curl --silent --insecure -X GET "http://${host}:${port}/arbitrary/${service}/${name}?rebuild=${rebuild}&filepath=${filepath}")
response=$(curl --silent --insecure -X GET "http://${host}:${port}/arbitrary/${service}/${name}?rebuild=${rebuild}&filepath=${filepath}" -H "X-API-KEY: ${apikey}")
fi
if [ -z "${response}" ]; then

View File

@@ -10,6 +10,8 @@ example_host=node10.qortal.org
# called-as name
name="${0##*/}"
script_dir=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
while [ -n "$*" ]; do
case $1 in
-p)
@@ -65,6 +67,14 @@ fi
url=$1
shift
if [ -f "apikey.txt" ]; then
apikey=$(cat "apikey.txt")
elif [ -f "${script_dir}/../apikey.txt" ]; then
apikey=$(cat "${script_dir}/../apikey.txt")
elif [ -f "${HOME}/qortal/apikey.txt" ]; then
apikey=$(cat "${HOME}/qortal/apikey.txt")
fi
if [ "${url:0:4}" != "http" ]; then
base_url=${BASE_URL-localhost:${port}}
@@ -83,5 +93,5 @@ if [ "$#" != 0 ]; then
data="--data"
fi
curl --silent --insecure --connect-timeout 5 ${content_type:+--header} "${content_type}" ${method} ${src} --url ${url} ${data} "$@" | ${postproc}
curl --silent --insecure --connect-timeout 5 -H "X-API-KEY: ${apikey}" ${content_type:+--header} "${content_type}" ${method} ${src} --url ${url} ${data} "$@" | ${postproc}
echo