diff --git a/src/main/java/org/qortal/api/resource/ArbitraryResource.java b/src/main/java/org/qortal/api/resource/ArbitraryResource.java index 41a8d17f..10ea7005 100644 --- a/src/main/java/org/qortal/api/resource/ArbitraryResource.java +++ b/src/main/java/org/qortal/api/resource/ArbitraryResource.java @@ -27,6 +27,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.qortal.api.*; import org.qortal.api.resource.TransactionsResource.ConfirmationStatus; +import org.qortal.arbitrary.ArbitraryDataTransactionBuilder; import org.qortal.block.BlockChain; import org.qortal.crypto.Crypto; import org.qortal.data.PaymentData; @@ -255,7 +256,10 @@ public class ArbitraryResource { } ) @ApiErrors({ApiError.REPOSITORY_ISSUE}) - public String uploadFileAtPath(@PathParam("publickey") String creatorPublicKeyBase58, String path) { + public String uploadFileAtPath(@PathParam("method") String methodString, + @PathParam("publickey") String publicKey58, + @PathParam("name") String name, + String path) { Security.checkApiCallAllowed(request); // It's too dangerous to allow user-supplied filenames in weaker security contexts @@ -263,84 +267,16 @@ public class ArbitraryResource { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); } - ArbitraryDataFile arbitraryDataFile = null; - try (final Repository repository = RepositoryManager.getRepository()) { + try { + ArbitraryDataTransactionBuilder transactionBuilder = new ArbitraryDataTransactionBuilder( + publicKey58, Paths.get(path), name, Method.valueOf(methodString), Service.ARBITRARY_DATA + ); - if (creatorPublicKeyBase58 == null || path == null) { - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); - } - byte[] creatorPublicKey = Base58.decode(creatorPublicKeyBase58); - final String creatorAddress = Crypto.toAddress(creatorPublicKey); - byte[] lastReference = repository.getAccountRepository().getLastReference(creatorAddress); - if (lastReference == null) { - // Use a random last reference on the very first transaction for an account - // Code copied from CrossChainResource.buildAtMessage() - // We already require PoW on all arbitrary transactions, so no additional logic is needed - Random random = new Random(); - lastReference = new byte[Transformer.SIGNATURE_LENGTH]; - random.nextBytes(lastReference); - } - - String name = null; - byte[] secret = null; - Method method = Method.PUT; - Service service = Service.ARBITRARY_DATA; - Compression compression = Compression.NONE; - - ArbitraryDataWriter arbitraryDataWriter = new ArbitraryDataWriter(Paths.get(path), name, service, method, compression); - try { - arbitraryDataWriter.save(); - } catch (IOException | DataException | InterruptedException e) { - LOGGER.info("Unable to create arbitrary data file: {}", e.getMessage()); - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE); - } catch (RuntimeException e) { - LOGGER.info("Unable to create arbitrary data file: {}", e.getMessage()); - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); - } - - String digest58 = arbitraryDataFile.digest58(); - if (digest58 == null) { - LOGGER.error("Unable to calculate digest"); - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); - } - - final BaseTransactionData baseTransactionData = new BaseTransactionData(NTP.getTime(), Group.NO_GROUP, - lastReference, creatorPublicKey, BlockChain.getInstance().getUnitFee(), null); - final int size = (int) arbitraryDataFile.size(); - final int version = 5; - final int nonce = 0; - final ArbitraryTransactionData.DataType dataType = ArbitraryTransactionData.DataType.DATA_HASH; - final byte[] digest = arbitraryDataFile.digest(); - final byte[] chunkHashes = arbitraryDataFile.chunkHashes(); - final List payments = new ArrayList<>(); - - ArbitraryTransactionData transactionData = new ArbitraryTransactionData(baseTransactionData, - version, service, nonce, size, name, method, - secret, compression, digest, dataType, chunkHashes, payments); - - ArbitraryTransaction transaction = (ArbitraryTransaction) Transaction.fromData(repository, transactionData); - transaction.computeNonce(); - - Transaction.ValidationResult result = transaction.isValidUnconfirmed(); - if (result != Transaction.ValidationResult.OK) { - arbitraryDataFile.deleteAll(); - throw TransactionsResource.createTransactionInvalidException(request, result); - } - - byte[] bytes = ArbitraryTransactionTransformer.toBytes(transactionData); + byte[] bytes = transactionBuilder.build(); return Base58.encode(bytes); } catch (DataException e) { - arbitraryDataFile.deleteAll(); - LOGGER.error("Repository issue when uploading data", e); - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); - } catch (TransformationException e) { - arbitraryDataFile.deleteAll(); - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e); - } catch (IllegalStateException e) { - arbitraryDataFile.deleteAll(); - LOGGER.error("Invalid upload data", e); - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA, e); + throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_DATA, e.getMessage()); } } diff --git a/src/main/java/org/qortal/api/resource/WebsiteResource.java b/src/main/java/org/qortal/api/resource/WebsiteResource.java index 5d85a7c6..16ab7d9c 100644 --- a/src/main/java/org/qortal/api/resource/WebsiteResource.java +++ b/src/main/java/org/qortal/api/resource/WebsiteResource.java @@ -31,6 +31,7 @@ import org.qortal.api.ApiError; import org.qortal.api.ApiExceptionFactory; import org.qortal.api.HTMLParser; import org.qortal.api.Security; +import org.qortal.arbitrary.ArbitraryDataTransactionBuilder; import org.qortal.block.BlockChain; import org.qortal.crypto.Crypto; import org.qortal.data.PaymentData; @@ -91,92 +92,27 @@ public class WebsiteResource { ) } ) - public String uploadWebsite(@PathParam("method") String methodString, @PathParam("publickey") String publicKey58, @PathParam("name") String name, String path) { + public String uploadWebsite(@PathParam("method") String methodString, + @PathParam("publickey") String publicKey58, + @PathParam("name") String name, + String path) { Security.checkApiCallAllowed(request); - // It's too dangerous to allow user-supplied filenames in weaker security contexts + // It's too dangerous to allow user-supplied file paths in weaker security contexts if (Settings.getInstance().isApiRestricted()) { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); } - ArbitraryDataFile arbitraryDataFile = null; - try (final Repository repository = RepositoryManager.getRepository()) { + try { + ArbitraryDataTransactionBuilder transactionBuilder = new ArbitraryDataTransactionBuilder( + publicKey58, Paths.get(path), name, Method.valueOf(methodString), Service.WEBSITE + ); - if (publicKey58 == null || path == null) { - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); - } - byte[] creatorPublicKey = Base58.decode(publicKey58); - final String creatorAddress = Crypto.toAddress(creatorPublicKey); - byte[] lastReference = repository.getAccountRepository().getLastReference(creatorAddress); - if (lastReference == null) { - // Use a random last reference on the very first transaction for an account - // Code copied from CrossChainResource.buildAtMessage() - // We already require PoW on all arbitrary transactions, so no additional logic is needed - Random random = new Random(); - lastReference = new byte[Transformer.SIGNATURE_LENGTH]; - random.nextBytes(lastReference); - } - - ArbitraryTransactionData.Method method = ArbitraryTransactionData.Method.valueOf(methodString); - ArbitraryTransactionData.Service service = ArbitraryTransactionData.Service.WEBSITE; - ArbitraryTransactionData.Compression compression = ArbitraryTransactionData.Compression.ZIP; - - ArbitraryDataWriter arbitraryDataWriter = new ArbitraryDataWriter(Paths.get(path), name, service, method, compression); - try { - arbitraryDataWriter.save(); - } catch (IOException | DataException | InterruptedException e) { - LOGGER.info("Unable to create arbitrary data file: {}", e.getMessage()); - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE); - } catch (RuntimeException e) { - LOGGER.info("Unable to create arbitrary data file: {}", e.getMessage()); - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); - } - - arbitraryDataFile = arbitraryDataWriter.getArbitraryDataFile(); - if (arbitraryDataFile == null) { - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); - } - - String digest58 = arbitraryDataFile.digest58(); - if (digest58 == null) { - LOGGER.error("Unable to calculate digest"); - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); - } - - final BaseTransactionData baseTransactionData = new BaseTransactionData(NTP.getTime(), Group.NO_GROUP, - lastReference, creatorPublicKey, BlockChain.getInstance().getUnitFee(), null); - final int size = (int) arbitraryDataFile.size(); - final int version = 5; - final int nonce = 0; - byte[] secret = arbitraryDataFile.getSecret(); - final ArbitraryTransactionData.DataType dataType = ArbitraryTransactionData.DataType.DATA_HASH; - final byte[] digest = arbitraryDataFile.digest(); - final byte[] chunkHashes = arbitraryDataFile.chunkHashes(); - final List payments = new ArrayList<>(); - - ArbitraryTransactionData transactionData = new ArbitraryTransactionData(baseTransactionData, - version, service, nonce, size, name, method, - secret, compression, digest, dataType, chunkHashes, payments); - - ArbitraryTransaction transaction = (ArbitraryTransaction) Transaction.fromData(repository, transactionData); - LOGGER.info("Computing nonce..."); - transaction.computeNonce(); - - Transaction.ValidationResult result = transaction.isValidUnconfirmed(); - if (result != Transaction.ValidationResult.OK) { - arbitraryDataFile.deleteAll(); - throw TransactionsResource.createTransactionInvalidException(request, result); - } - - byte[] bytes = ArbitraryTransactionTransformer.toBytes(transactionData); + byte[] bytes = transactionBuilder.build(); return Base58.encode(bytes); - } catch (TransformationException e) { - arbitraryDataFile.deleteAll(); - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e); } catch (DataException e) { - arbitraryDataFile.deleteAll(); - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_DATA, e.getMessage()); } } diff --git a/src/main/java/org/qortal/arbitrary/ArbitraryDataTransactionBuilder.java b/src/main/java/org/qortal/arbitrary/ArbitraryDataTransactionBuilder.java new file mode 100644 index 00000000..28b58a2b --- /dev/null +++ b/src/main/java/org/qortal/arbitrary/ArbitraryDataTransactionBuilder.java @@ -0,0 +1,121 @@ +package org.qortal.arbitrary; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.qortal.block.BlockChain; +import org.qortal.crypto.Crypto; +import org.qortal.data.PaymentData; +import org.qortal.data.transaction.ArbitraryTransactionData; +import org.qortal.data.transaction.ArbitraryTransactionData.*; +import org.qortal.data.transaction.BaseTransactionData; +import org.qortal.group.Group; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; +import org.qortal.transaction.ArbitraryTransaction; +import org.qortal.transaction.Transaction; +import org.qortal.transform.TransformationException; +import org.qortal.transform.Transformer; +import org.qortal.transform.transaction.ArbitraryTransactionTransformer; +import org.qortal.utils.Base58; +import org.qortal.utils.NTP; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +public class ArbitraryDataTransactionBuilder { + + private static final Logger LOGGER = LogManager.getLogger(ArbitraryDataTransactionBuilder.class); + + private String publicKey58; + private Path path; + private String name; + private Method method; + private Service service; + + public ArbitraryDataTransactionBuilder(String publicKey58, Path path, String name, Method method, Service service) { + this.publicKey58 = publicKey58; + this.path = path; + this.name = name; + this.method = method; + this.service = service; + } + + public byte[] build() throws DataException { + ArbitraryDataFile arbitraryDataFile = null; + try (final Repository repository = RepositoryManager.getRepository()) { + + if (publicKey58 == null || path == null) { + throw new DataException("Missing public key or path"); + } + byte[] creatorPublicKey = Base58.decode(publicKey58); + final String creatorAddress = Crypto.toAddress(creatorPublicKey); + byte[] lastReference = repository.getAccountRepository().getLastReference(creatorAddress); + if (lastReference == null) { + // Use a random last reference on the very first transaction for an account + // Code copied from CrossChainResource.buildAtMessage() + // We already require PoW on all arbitrary transactions, so no additional logic is needed + Random random = new Random(); + lastReference = new byte[Transformer.SIGNATURE_LENGTH]; + random.nextBytes(lastReference); + } + + ArbitraryTransactionData.Compression compression = ArbitraryTransactionData.Compression.ZIP; + + ArbitraryDataWriter arbitraryDataWriter = new ArbitraryDataWriter(path, name, service, method, compression); + try { + arbitraryDataWriter.save(); + } catch (IOException | DataException | InterruptedException | RuntimeException e) { + LOGGER.info("Unable to create arbitrary data file: {}", e.getMessage()); + throw new DataException(String.format("Unable to create arbitrary data file: %s", e.getMessage())); + } + + arbitraryDataFile = arbitraryDataWriter.getArbitraryDataFile(); + if (arbitraryDataFile == null) { + throw new DataException("Arbitrary data file is null"); + } + + String digest58 = arbitraryDataFile.digest58(); + if (digest58 == null) { + LOGGER.error("Unable to calculate file digest"); + throw new DataException("Unable to calculate file digest"); + } + + final BaseTransactionData baseTransactionData = new BaseTransactionData(NTP.getTime(), Group.NO_GROUP, + lastReference, creatorPublicKey, BlockChain.getInstance().getUnitFee(), null); + final int size = (int) arbitraryDataFile.size(); + final int version = 5; + final int nonce = 0; + byte[] secret = arbitraryDataFile.getSecret(); + final ArbitraryTransactionData.DataType dataType = ArbitraryTransactionData.DataType.DATA_HASH; + final byte[] digest = arbitraryDataFile.digest(); + final byte[] chunkHashes = arbitraryDataFile.chunkHashes(); + final List payments = new ArrayList<>(); + + ArbitraryTransactionData transactionData = new ArbitraryTransactionData(baseTransactionData, + version, service, nonce, size, name, method, + secret, compression, digest, dataType, chunkHashes, payments); + + ArbitraryTransaction transaction = (ArbitraryTransaction) Transaction.fromData(repository, transactionData); + LOGGER.info("Computing nonce..."); + transaction.computeNonce(); + + Transaction.ValidationResult result = transaction.isValidUnconfirmed(); + if (result != Transaction.ValidationResult.OK) { + arbitraryDataFile.deleteAll(); + throw new DataException(String.format("Arbitrary transaction invalid: %s", result)); + } + + return ArbitraryTransactionTransformer.toBytes(transactionData); + + } catch (TransformationException | DataException e) { + arbitraryDataFile.deleteAll(); + throw new DataException(String.format("Unable to build ARBITRARY transaction: %s", e.getMessage())); + } + + } + +}