diff --git a/pom.xml b/pom.xml index 7a781439..e9cbf1d4 100644 --- a/pom.xml +++ b/pom.xml @@ -7,6 +7,13 @@ <packaging>jar</packaging> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <bouncycastle.version>1.60</bouncycastle.version> + <hsqldb.version>r5836</hsqldb.version> + <jetty.version>9.4.11.v20180605</jetty.version> + <jersey.version>2.27</jersey.version> + <log4j.version>2.11.0</log4j.version> + <slf4j.version>1.7.12</slf4j.version> + <swagger-api.version>2.0.6</swagger-api.version> <swagger-ui.version>3.19.0</swagger-ui.version> </properties> <build> @@ -156,105 +163,144 @@ </repository> </repositories> <dependencies> + <!-- HSQLDB for repository --> <dependency> <groupId>org.hsqldb</groupId> <artifactId>hsqldb</artifactId> - <version>r5836</version> + <version>${hsqldb.version}</version> </dependency> + <!-- CIYAM AT (automated transactions) --> + <dependency> + <groupId>org.ciyam</groupId> + <artifactId>at</artifactId> + <version>1.0</version> + </dependency> + <!-- Bitcoin support --> + <dependency> + <groupId>org.bitcoinj</groupId> + <artifactId>bitcoinj-core</artifactId> + <version>0.14.7</version> + </dependency> + <!-- Utilities --> <dependency> <groupId>com.googlecode.json-simple</groupId> <artifactId>json-simple</artifactId> <version>1.1.1</version> </dependency> - <dependency> - <groupId>com.google.guava</groupId> - <artifactId>guava</artifactId> - <version>25.0-jre</version> - </dependency> - <dependency> - <groupId>org.apache.logging.log4j</groupId> - <artifactId>log4j-core</artifactId> - <version>2.11.0</version> - </dependency> - <dependency> - <groupId>org.apache.logging.log4j</groupId> - <artifactId>log4j-api</artifactId> - <version>2.11.0</version> - </dependency> - <dependency> - <groupId>commons-net</groupId> - <artifactId>commons-net</artifactId> - <version>3.3</version> - </dependency> - <dependency> - <groupId>org.glassfish.jersey.core</groupId> - <artifactId>jersey-server</artifactId> - <version>2.27</version> - </dependency> - <dependency> - <groupId>javax.servlet</groupId> - <artifactId>javax.servlet-api</artifactId> - <version>4.0.1</version> - </dependency> - <dependency> - <groupId>org.eclipse.jetty</groupId> - <artifactId>jetty-server</artifactId> - <version>9.4.11.v20180605</version> - <classifier>config</classifier> - </dependency> - <dependency> - <groupId>org.glassfish.jersey.containers</groupId> - <artifactId>jersey-container-servlet-core</artifactId> - <version>2.27</version> - </dependency> - <dependency> - <groupId>org.eclipse.jetty</groupId> - <artifactId>jetty-servlet</artifactId> - <version>9.4.11.v20180605</version> - <type>jar</type> - </dependency> - <!-- https://mvnrepository.com/artifact/org.eclipse.jetty/jetty-servlets --> - <dependency> - <groupId>org.eclipse.jetty</groupId> - <artifactId>jetty-servlets</artifactId> - <version>9.4.11.v20180605</version> - </dependency> - <dependency> - <groupId>org.glassfish.jersey.inject</groupId> - <artifactId>jersey-hk2</artifactId> - <version>2.27</version> - </dependency> - <dependency> - <groupId>io.swagger.core.v3</groupId> - <artifactId>swagger-jaxrs2</artifactId> - <version>2.0.4</version> - </dependency> - <dependency> - <groupId>io.swagger.core.v3</groupId> - <artifactId>swagger-jaxrs2-servlet-initializer</artifactId> - <version>2.0.4</version> - </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-text</artifactId> <version>1.4</version> </dependency> + <dependency> + <groupId>commons-net</groupId> + <artifactId>commons-net</artifactId> + <version>3.3</version> + </dependency> + <dependency> + <groupId>com.google.guava</groupId> + <artifactId>guava</artifactId> + <version>25.0-jre</version> + </dependency> + <!-- Logging: log4j2 --> + <dependency> + <groupId>org.apache.logging.log4j</groupId> + <artifactId>log4j-core</artifactId> + <version>${log4j.version}</version> + </dependency> + <dependency> + <groupId>org.apache.logging.log4j</groupId> + <artifactId>log4j-api</artifactId> + <version>${log4j.version}</version> + </dependency> + <!-- Logging: slf4j used by Jetty --> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + <version>${slf4j.version}</version> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-simple</artifactId> + <version>${slf4j.version}</version> + </dependency> + <!-- Servlet related --> + <dependency> + <groupId>javax.servlet</groupId> + <artifactId>javax.servlet-api</artifactId> + <version>4.0.1</version> + </dependency> + <dependency> + <groupId>javax.mail</groupId> + <artifactId>mail</artifactId> + <version>1.5.0-b01</version> + </dependency> + <!-- Jetty --> + <dependency> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-server</artifactId> + <version>${jetty.version}</version> + <classifier>config</classifier> + </dependency> + <dependency> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-servlet</artifactId> + <version>${jetty.version}</version> + <type>jar</type> + </dependency> + <dependency> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-servlets</artifactId> + <version>${jetty.version}</version> + </dependency> + <dependency> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-rewrite</artifactId> + <version>${jetty.version}</version> + </dependency> + <!-- Jersey --> + <dependency> + <groupId>org.glassfish.jersey.core</groupId> + <artifactId>jersey-server</artifactId> + <version>${jersey.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>jersey-container-servlet-core</artifactId> + <version>${jersey.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.inject</groupId> + <artifactId>jersey-hk2</artifactId> + <version>${jersey.version}</version> + </dependency> <dependency> <groupId>org.glassfish.jersey.media</groupId> <artifactId>jersey-media-moxy</artifactId> - <version>2.27</version> + <version>${jersey.version}</version> </dependency> <dependency> - <groupId>org.ciyam</groupId> - <artifactId>at</artifactId> - <version>1.0</version> + <groupId>org.glassfish.jersey.media</groupId> + <artifactId>jersey-media-multipart</artifactId> + <version>${jersey.version}</version> + </dependency> + <!-- Swagger OpenAPI implementation --> + <dependency> + <groupId>io.swagger.core.v3</groupId> + <artifactId>swagger-jaxrs2</artifactId> + <version>${swagger-api.version}</version> </dependency> <dependency> - <groupId>org.hsqldb</groupId> - <artifactId>sqltool</artifactId> - <version>2.4.1</version> - <scope>test</scope> + <groupId>io.swagger.core.v3</groupId> + <artifactId>swagger-jaxrs2-servlet-initializer</artifactId> + <version>${swagger-api.version}</version> </dependency> + <dependency> + <groupId>org.webjars</groupId> + <artifactId>swagger-ui</artifactId> + <version>${swagger-ui.version}</version> + </dependency> + <!-- Testing --> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-engine</artifactId> @@ -265,48 +311,11 @@ <artifactId>hamcrest-library</artifactId> <version>1.3</version> </dependency> - <dependency> - <groupId>org.glassfish.jersey.media</groupId> - <artifactId>jersey-media-multipart</artifactId> - <version>2.27</version> - </dependency> - <dependency> - <groupId>javax.mail</groupId> - <artifactId>mail</artifactId> - <version>1.5.0-b01</version> - </dependency> - <dependency> - <groupId>org.webjars</groupId> - <artifactId>swagger-ui</artifactId> - <version>${swagger-ui.version}</version> - </dependency> - <dependency> - <groupId>org.eclipse.jetty</groupId> - <artifactId>jetty-rewrite</artifactId> - <version>9.4.11.v20180605</version> - </dependency> - <dependency> - <groupId>org.bitcoinj</groupId> - <artifactId>bitcoinj-core</artifactId> - <version>0.14.7</version> - </dependency> - <!-- https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk15on --> + <!-- BouncyCastle for crypto, including TLS secure networking --> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> - <version>1.60</version> - </dependency> - <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api --> - <dependency> - <groupId>org.slf4j</groupId> - <artifactId>slf4j-api</artifactId> - <version>1.7.12</version> - </dependency> - <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-simple --> - <dependency> - <groupId>org.slf4j</groupId> - <artifactId>slf4j-simple</artifactId> - <version>1.7.12</version> + <version>${bouncycastle.version}</version> </dependency> </dependencies> </project> \ No newline at end of file diff --git a/src/api/AddressesResource.java b/src/api/AddressesResource.java index dad51a40..9d335e68 100644 --- a/src/api/AddressesResource.java +++ b/src/api/AddressesResource.java @@ -81,8 +81,6 @@ public class AddressesResource { public String getLastReference( @Parameter(description = "a base58-encoded address", required = true) @PathParam("address") String address ) { - Security.checkApiCallAllowed("GET addresses/lastreference", request); - if (!Crypto.isValidAddress(address)) throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS); @@ -130,8 +128,6 @@ public class AddressesResource { } ) public String getLastReferenceUnconfirmed(@PathParam("address") String address) { - Security.checkApiCallAllowed("GET addresses/lastreference", request); - if (!Crypto.isValidAddress(address)) throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS); @@ -177,8 +173,6 @@ public class AddressesResource { } ) public boolean validate(@PathParam("address") String address) { - Security.checkApiCallAllowed("GET addresses/validate", request); - return Crypto.isValidAddress(address); } @@ -208,8 +202,6 @@ public class AddressesResource { } ) public BigDecimal getGeneratingBalanceOfAddress(@PathParam("address") String address) { - Security.checkApiCallAllowed("GET addresses/generatingbalance", request); - if (!Crypto.isValidAddress(address)) throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS); @@ -250,8 +242,6 @@ public class AddressesResource { } ) public BigDecimal getGeneratingBalance(@PathParam("address") String address) { - Security.checkApiCallAllowed("GET addresses/balance", request); - if (!Crypto.isValidAddress(address)) throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS); @@ -291,8 +281,6 @@ public class AddressesResource { } ) public BigDecimal getAssetBalance(@PathParam("assetid") long assetid, @PathParam("address") String address) { - Security.checkApiCallAllowed("GET addresses/assetbalance", request); - if (!Crypto.isValidAddress(address)) throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS); @@ -332,8 +320,6 @@ public class AddressesResource { } ) public List<AccountBalanceData> getAssets(@PathParam("address") String address) { - Security.checkApiCallAllowed("GET addresses/assets", request); - if (!Crypto.isValidAddress(address)) throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS); @@ -372,8 +358,6 @@ public class AddressesResource { } ) public String getGeneratingBalance(@PathParam("address") String address, @PathParam("confirmations") int confirmations) { - Security.checkApiCallAllowed("GET addresses/balance", request); - throw new UnsupportedOperationException(); } @@ -403,8 +387,6 @@ public class AddressesResource { } ) public String getPublicKey(@PathParam("address") String address) { - Security.checkApiCallAllowed("GET addresses/publickey", request); - throw new UnsupportedOperationException(); } diff --git a/src/api/AdminResource.java b/src/api/AdminResource.java index 0f8a006d..ffb238f4 100644 --- a/src/api/AdminResource.java +++ b/src/api/AdminResource.java @@ -1,7 +1,8 @@ package api; -import globalization.Translator; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.extensions.Extension; import io.swagger.v3.oas.annotations.extensions.ExtensionProperty; import io.swagger.v3.oas.annotations.media.Content; @@ -30,16 +31,14 @@ public class AdminResource { @Context HttpServletRequest request; - private static final long startTime = System.currentTimeMillis(); - - private ApiErrorFactory apiErrorFactory; - - public AdminResource() { - this(new ApiErrorFactory(Translator.getInstance())); - } - - public AdminResource(ApiErrorFactory apiErrorFactory) { - this.apiErrorFactory = apiErrorFactory; + @GET + @Path("/dud") + @Parameter(name = "blockSignature", description = "Block signature", schema = @Schema(type = "string", format = "byte", minLength = 84, maxLength=88)) + @Parameter(in = ParameterIn.QUERY, name = "limit", description = "Maximum number of entries to return", schema = @Schema(type = "integer", defaultValue = "10")) + @Parameter(in = ParameterIn.QUERY, name = "offset", description = "Starting entry in results", schema = @Schema(type = "integer")) + @Parameter(in = ParameterIn.QUERY, name = "includeTransactions", description = "Include associated transactions in results", schema = @Schema(type = "boolean")) + public String globalParameters() { + return ""; } @GET @@ -65,9 +64,7 @@ public class AdminResource { } ) public String uptime() { - Security.checkApiCallAllowed("GET admin/uptime", request); - - return Long.toString(System.currentTimeMillis() - startTime); + return Long.toString(System.currentTimeMillis() - Controller.startTime); } @GET @@ -93,16 +90,16 @@ public class AdminResource { } ) public String shutdown() { - Security.checkApiCallAllowed("GET admin/stop", request); + Security.checkApiCallAllowed(request); new Thread(new Runnable() { @Override public void run() { Controller.shutdown(); } - }); // disabled for now: .start(); + }).start(); - return "false"; + return "true"; } } diff --git a/src/api/AnnotationPostProcessor.java b/src/api/AnnotationPostProcessor.java index f34413fe..f4b2f22a 100644 --- a/src/api/AnnotationPostProcessor.java +++ b/src/api/AnnotationPostProcessor.java @@ -6,23 +6,31 @@ import globalization.Translator; import io.swagger.v3.core.converter.ModelConverters; import io.swagger.v3.jaxrs2.Reader; import io.swagger.v3.jaxrs2.ReaderListener; -import io.swagger.v3.oas.models.media.Content; +import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.Operation; import io.swagger.v3.oas.models.PathItem; import io.swagger.v3.oas.models.examples.Example; import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.media.Content; import io.swagger.v3.oas.models.media.MediaType; import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.parameters.Parameter; import io.swagger.v3.oas.models.responses.ApiResponse; + import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + public class AnnotationPostProcessor implements ReaderListener { + private static final Logger LOGGER = LogManager.getLogger(AnnotationPostProcessor.class); + private class ContextInformation { public String path; public Map<String, String> keys; @@ -41,10 +49,22 @@ public class AnnotationPostProcessor implements ReaderListener { } @Override - public void beforeScan(Reader reader, OpenAPI openAPI) {} + public void beforeScan(Reader reader, OpenAPI openAPI) { + LOGGER.info("beforeScan"); + } @Override public void afterScan(Reader reader, OpenAPI openAPI) { + LOGGER.info("afterScan"); + + // Populate Components section with reusable parameters, like "limit" and "offset" + // We take the reusable parameters from AdminResource.globalParameters path "/admin/dud" + Components components = openAPI.getComponents(); + PathItem globalParametersPathItem = openAPI.getPaths().get("/admin/dud"); + if (globalParametersPathItem != null) + for (Parameter parameter : globalParametersPathItem.getGet().getParameters()) + components.addParameters(parameter.getName(), parameter); + // use context path and keys from "x-translation" extension annotations // to translate supported annotations and finally remove "x-translation" extensions Info resourceInfo = openAPI.getInfo(); diff --git a/src/api/ApiService.java b/src/api/ApiService.java index f68453e0..2389eff6 100644 --- a/src/api/ApiService.java +++ b/src/api/ApiService.java @@ -31,10 +31,12 @@ public class ApiService { this.resources.add(AdminResource.class); this.resources.add(BlocksResource.class); this.resources.add(TransactionsResource.class); - this.resources.add(BlockExplorerResource.class); - this.resources.add(OpenApiResource.class); // swagger - this.resources.add(ApiDefinition.class); // for API definition - this.resources.add(AnnotationPostProcessor.class); // for API resource annotations + this.resources.add(UtilsResource.class); + + this.resources.add(BlockExplorerResource.class); // block-explorer.html + this.resources.add(OpenApiResource.class); // Swagger/OpenAPI + this.resources.add(ApiDefinition.class); // API info + this.resources.add(AnnotationPostProcessor.class); // For API resource annotations ResourceConfig config = new ResourceConfig(this.resources); // Create RPC server diff --git a/src/api/BlockExplorerResource.java b/src/api/BlockExplorerResource.java index e8720c11..6944ced0 100644 --- a/src/api/BlockExplorerResource.java +++ b/src/api/BlockExplorerResource.java @@ -7,12 +7,11 @@ import java.nio.file.Files; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.GET; import javax.ws.rs.Path; -import javax.ws.rs.Produces; import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; + +import io.swagger.v3.oas.annotations.Operation; @Path("/") -@Produces({ MediaType.TEXT_HTML }) public class BlockExplorerResource { @Context @@ -23,6 +22,7 @@ public class BlockExplorerResource { @GET @Path("/block-explorer.html") + @Operation(hidden = true) public String getBlockExplorer() { try { byte[] htmlBytes = Files.readAllBytes(FileSystems.getDefault().getPath("block-explorer.html")); diff --git a/src/api/BlocksResource.java b/src/api/BlocksResource.java index dfbfed37..271ad9a7 100644 --- a/src/api/BlocksResource.java +++ b/src/api/BlocksResource.java @@ -3,6 +3,7 @@ package api; import data.block.BlockData; import globalization.Translator; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.extensions.Extension; import io.swagger.v3.oas.annotations.extensions.ExtensionProperty; import io.swagger.v3.oas.annotations.media.Content; @@ -11,19 +12,23 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import java.math.BigDecimal; +import java.util.Base64; + import javax.servlet.http.HttpServletRequest; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; + +import api.models.BlockWithTransactions; import qora.block.Block; import repository.DataException; import repository.Repository; import repository.RepositoryManager; -import utils.Base58; @Path("blocks") @Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN}) @@ -50,7 +55,7 @@ public class BlocksResource { @GET @Path("/{signature}") @Operation( - summary = "Fetch block using base58 signature", + summary = "Fetch block using base64 signature", description = "returns the block that matches the given signature", extensions = { @Extension(name = "translation", properties = { @@ -64,7 +69,7 @@ public class BlocksResource { responses = { @ApiResponse( description = "the block", - content = @Content(schema = @Schema(implementation = BlockData.class)), + content = @Content(schema = @Schema(implementation = BlockWithTransactions.class)), extensions = { @Extension(name = "translation", properties = { @ExtensionProperty(name="description.key", value="success_response:description") @@ -73,34 +78,23 @@ public class BlocksResource { ) } ) - public BlockData getBlock(@PathParam("signature") String signature) { - Security.checkApiCallAllowed("GET blocks", request); - - // decode signature + public BlockWithTransactions getBlock(@PathParam("signature") String signature, @Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) { + // Decode signature byte[] signatureBytes; - try - { - signatureBytes = Base58.decode(signature); - } - catch(Exception e) - { - throw this.apiErrorFactory.createError(ApiError.INVALID_SIGNATURE, e); + try { + signatureBytes = Base64.getDecoder().decode(signature); + } catch (NumberFormatException e) { + throw this.apiErrorFactory.createError(ApiError.INVALID_SIGNATURE, e); } - try (final Repository repository = RepositoryManager.getRepository()) { - BlockData blockData = repository.getBlockRepository().fromSignature(signatureBytes); - - // check if block exists - if(blockData == null) - throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS); - - return blockData; - + try (final Repository repository = RepositoryManager.getRepository()) { + BlockData blockData = repository.getBlockRepository().fromSignature(signatureBytes); + return new BlockWithTransactions(repository, blockData, includeTransactions); } catch (ApiException e) { throw e; - } catch (Exception e) { - throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e); - } + } catch (DataException e) { + throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e); + } } @GET @@ -115,7 +109,7 @@ public class BlocksResource { responses = { @ApiResponse( description = "the block", - content = @Content(schema = @Schema(implementation = BlockData.class)), + content = @Content(schema = @Schema(implementation = BlockWithTransactions.class)), extensions = { @Extension(name = "translation", properties = { @ExtensionProperty(name="description.key", value="success_response:description") @@ -124,18 +118,15 @@ public class BlocksResource { ) } ) - public BlockData getFirstBlock() { - Security.checkApiCallAllowed("GET blocks/first", request); - - try (final Repository repository = RepositoryManager.getRepository()) { - BlockData blockData = repository.getBlockRepository().fromHeight(1); - return blockData; - + public BlockWithTransactions getFirstBlock(@Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) { + try (final Repository repository = RepositoryManager.getRepository()) { + BlockData blockData = repository.getBlockRepository().fromHeight(1); + return new BlockWithTransactions(repository, blockData, includeTransactions); } catch (ApiException e) { throw e; - } catch (Exception e) { - throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e); - } + } catch (DataException e) { + throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e); + } } @GET @@ -150,7 +141,7 @@ public class BlocksResource { responses = { @ApiResponse( description = "the block", - content = @Content(schema = @Schema(implementation = BlockData.class)), + content = @Content(schema = @Schema(implementation = BlockWithTransactions.class)), extensions = { @Extension(name = "translation", properties = { @ExtensionProperty(name="description.key", value="success_response:description") @@ -159,24 +150,21 @@ public class BlocksResource { ) } ) - public BlockData getLastBlock() { - Security.checkApiCallAllowed("GET blocks/last", request); - - try (final Repository repository = RepositoryManager.getRepository()) { - BlockData blockData = repository.getBlockRepository().getLastBlock(); - return blockData; - + public BlockWithTransactions getLastBlock(@Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) { + try (final Repository repository = RepositoryManager.getRepository()) { + BlockData blockData = repository.getBlockRepository().getLastBlock(); + return new BlockWithTransactions(repository, blockData, includeTransactions); } catch (ApiException e) { throw e; - } catch (Exception e) { - throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e); - } + } catch (DataException e) { + throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e); + } } @GET @Path("/child/{signature}") @Operation( - summary = "Fetch child block using base58 signature of parent block", + summary = "Fetch child block using base64 signature of parent block", description = "returns the child block of the block that matches the given signature", extensions = { @Extension(name = "translation", properties = { @@ -190,7 +178,7 @@ public class BlocksResource { responses = { @ApiResponse( description = "the block", - content = @Content(schema = @Schema(implementation = BlockData.class)), + content = @Content(schema = @Schema(implementation = BlockWithTransactions.class)), extensions = { @Extension(name = "translation", properties = { @ExtensionProperty(name="description.key", value="success_response:description") @@ -199,13 +187,11 @@ public class BlocksResource { ) } ) - public BlockData getChild(@PathParam("signature") String signature) { - Security.checkApiCallAllowed("GET blocks/child", request); - - // decode signature + public BlockWithTransactions getChild(@PathParam("signature") String signature, @Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) { + // Decode signature byte[] signatureBytes; try { - signatureBytes = Base58.decode(signature); + signatureBytes = Base64.getDecoder().decode(signature); } catch (NumberFormatException e) { throw this.apiErrorFactory.createError(ApiError.INVALID_SIGNATURE, e); } @@ -213,17 +199,17 @@ public class BlocksResource { try (final Repository repository = RepositoryManager.getRepository()) { BlockData blockData = repository.getBlockRepository().fromSignature(signatureBytes); - // check if block exists + // Check block exists if(blockData == null) throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS); BlockData childBlockData = repository.getBlockRepository().fromReference(signatureBytes); - // check if child exists + // Check child exists if(childBlockData == null) throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS); - return childBlockData; + return new BlockWithTransactions(repository, childBlockData, includeTransactions); } catch (ApiException e) { throw e; } catch (DataException e) { @@ -252,18 +238,15 @@ public class BlocksResource { } ) public BigDecimal getGeneratingBalance() { - Security.checkApiCallAllowed("GET blocks/generatingbalance", request); - - try (final Repository repository = RepositoryManager.getRepository()) { - BlockData blockData = repository.getBlockRepository().getLastBlock(); + try (final Repository repository = RepositoryManager.getRepository()) { + BlockData blockData = repository.getBlockRepository().getLastBlock(); Block block = new Block(repository, blockData); return block.calcNextBlockGeneratingBalance(); - } catch (ApiException e) { throw e; - } catch (Exception e) { - throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e); - } + } catch (DataException e) { + throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e); + } } @GET @@ -292,34 +275,28 @@ public class BlocksResource { } ) public BigDecimal getGeneratingBalance(@PathParam("signature") String signature) { - Security.checkApiCallAllowed("GET blocks/generatingbalance", request); - - // decode signature + // Decode signature byte[] signatureBytes; - try - { - signatureBytes = Base58.decode(signature); - } - catch(Exception e) - { - throw this.apiErrorFactory.createError(ApiError.INVALID_SIGNATURE, e); + try { + signatureBytes = Base64.getDecoder().decode(signature); + } catch (NumberFormatException e) { + throw this.apiErrorFactory.createError(ApiError.INVALID_SIGNATURE, e); } - try (final Repository repository = RepositoryManager.getRepository()) { - BlockData blockData = repository.getBlockRepository().fromSignature(signatureBytes); - - // check if block exists - if(blockData == null) + try (final Repository repository = RepositoryManager.getRepository()) { + BlockData blockData = repository.getBlockRepository().fromSignature(signatureBytes); + + // Check block exists + if (blockData == null) throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS); Block block = new Block(repository, blockData); return block.calcNextBlockGeneratingBalance(); - } catch (ApiException e) { throw e; - } catch (Exception e) { - throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e); - } + } catch (DataException e) { + throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e); + } } @GET @@ -343,17 +320,14 @@ public class BlocksResource { } ) public long getTimePerBlock() { - Security.checkApiCallAllowed("GET blocks/time", request); - - try (final Repository repository = RepositoryManager.getRepository()) { - BlockData blockData = repository.getBlockRepository().getLastBlock(); + try (final Repository repository = RepositoryManager.getRepository()) { + BlockData blockData = repository.getBlockRepository().getLastBlock(); return Block.calcForgingDelay(blockData.getGeneratingBalance()); - } catch (ApiException e) { throw e; - } catch (Exception e) { - throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e); - } + } catch (DataException e) { + throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e); + } } @GET @@ -377,8 +351,6 @@ public class BlocksResource { } ) public long getTimePerBlock(@PathParam("generating") BigDecimal generatingbalance) { - Security.checkApiCallAllowed("GET blocks/time", request); - return Block.calcForgingDelay(generatingbalance); } @@ -403,16 +375,13 @@ public class BlocksResource { } ) public int getHeight() { - Security.checkApiCallAllowed("GET blocks/height", request); - - try (final Repository repository = RepositoryManager.getRepository()) { - return repository.getBlockRepository().getBlockchainHeight(); - + try (final Repository repository = RepositoryManager.getRepository()) { + return repository.getBlockRepository().getBlockchainHeight(); } catch (ApiException e) { throw e; - } catch (Exception e) { - throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e); - } + } catch (DataException e) { + throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e); + } } @GET @@ -441,33 +410,27 @@ public class BlocksResource { } ) public int getHeight(@PathParam("signature") String signature) { - Security.checkApiCallAllowed("GET blocks/height", request); - - // decode signature + // Decode signature byte[] signatureBytes; - try - { - signatureBytes = Base58.decode(signature); - } - catch(Exception e) - { - throw this.apiErrorFactory.createError(ApiError.INVALID_SIGNATURE, e); + try { + signatureBytes = Base64.getDecoder().decode(signature); + } catch (NumberFormatException e) { + throw this.apiErrorFactory.createError(ApiError.INVALID_SIGNATURE, e); } - try (final Repository repository = RepositoryManager.getRepository()) { - BlockData blockData = repository.getBlockRepository().fromSignature(signatureBytes); - - // check if block exists - if(blockData == null) + try (final Repository repository = RepositoryManager.getRepository()) { + BlockData blockData = repository.getBlockRepository().fromSignature(signatureBytes); + + // Check block exists + if (blockData == null) throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS); return blockData.getHeight(); - } catch (ApiException e) { throw e; - } catch (Exception e) { - throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e); - } + } catch (DataException e) { + throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e); + } } @GET @@ -486,7 +449,7 @@ public class BlocksResource { responses = { @ApiResponse( description = "the block", - content = @Content(schema = @Schema(implementation = BlockData.class)), + content = @Content(schema = @Schema(implementation = BlockWithTransactions.class)), extensions = { @Extension(name = "translation", properties = { @ExtensionProperty(name="description.key", value="success_response:description") @@ -495,22 +458,15 @@ public class BlocksResource { ) } ) - public BlockData getbyHeight(@PathParam("height") int height) { - Security.checkApiCallAllowed("GET blocks/byheight", request); - - try (final Repository repository = RepositoryManager.getRepository()) { - BlockData blockData = repository.getBlockRepository().fromHeight(height); - - // check if block exists - if(blockData == null) - throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS); - - return blockData; - + public BlockWithTransactions getbyHeight(@PathParam("height") int height, @Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) { + try (final Repository repository = RepositoryManager.getRepository()) { + BlockData blockData = repository.getBlockRepository().fromHeight(height); + return new BlockWithTransactions(repository, blockData, includeTransactions); } catch (ApiException e) { throw e; - } catch (Exception e) { - throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e); - } + } catch (DataException e) { + throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e); + } } + } diff --git a/src/api/Security.java b/src/api/Security.java index a7599d48..2d981b2c 100644 --- a/src/api/Security.java +++ b/src/api/Security.java @@ -1,12 +1,22 @@ package api; +import java.net.InetAddress; +import java.net.UnknownHostException; + import javax.servlet.http.HttpServletRequest; public class Security { - public static void checkApiCallAllowed(final String messageToDisplay, HttpServletRequest request) { - // TODO + // TODO: replace with proper authentication + public static void checkApiCallAllowed(HttpServletRequest request) { + InetAddress remoteAddr; + try { + remoteAddr = InetAddress.getByName(request.getRemoteAddr()); + } catch (UnknownHostException e) { + throw ApiErrorFactory.getInstance().createError(ApiError.UNAUTHORIZED); + } - // throw this.apiErrorFactory.createError(ApiError.UNAUTHORIZED); + if (!remoteAddr.isLoopbackAddress()) + throw ApiErrorFactory.getInstance().createError(ApiError.UNAUTHORIZED); } } diff --git a/src/api/TransactionsResource.java b/src/api/TransactionsResource.java index d396d3c3..a7ba240b 100644 --- a/src/api/TransactionsResource.java +++ b/src/api/TransactionsResource.java @@ -2,6 +2,8 @@ package api; import globalization.Translator; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.extensions.Extension; import io.swagger.v3.oas.annotations.extensions.ExtensionProperty; import io.swagger.v3.oas.annotations.media.ArraySchema; @@ -19,9 +21,12 @@ import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; +import data.transaction.GenesisTransactionData; +import data.transaction.PaymentTransactionData; import data.transaction.TransactionData; import repository.DataException; import repository.Repository; @@ -56,6 +61,9 @@ public class TransactionsResource { @Operation( summary = "Fetch transactions involving address", description = "Returns list of transactions", + parameters = { + @Parameter(in = ParameterIn.PATH, name = "address", description = "Account's address", schema = @Schema(type = "string")) + }, extensions = { @Extension(name = "translation", properties = { @ExtensionProperty(name="path", value="GET block:signature"), @@ -77,9 +85,7 @@ public class TransactionsResource { ) } ) - public List<TransactionData> getAddressTransactions(@PathParam("address") String address) { - Security.checkApiCallAllowed("GET transactions/address", request); - + public List<TransactionData> getAddressTransactions(@PathParam("address") String address, @Parameter(ref = "limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) { if (!Crypto.isValidAddress(address)) throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS); @@ -89,6 +95,9 @@ public class TransactionsResource { List<byte[]> signatures = txRepo.getAllSignaturesInvolvingAddress(address); // Pagination would take effect here (or as part of the repository access) + int fromIndex = Integer.min(offset, signatures.size()); + int toIndex = limit == 0 ? signatures.size() : Integer.min(fromIndex + limit, signatures.size()); + signatures = signatures.subList(fromIndex, toIndex); // Expand signatures to transactions List<TransactionData> transactions = new ArrayList<TransactionData>(signatures.size()); @@ -101,13 +110,12 @@ public class TransactionsResource { } catch (DataException e) { throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e); } - } @GET @Path("/block/{signature}") @Operation( - summary = "Fetch transactions via block signature", + summary = "Fetch transactions using block signature", description = "Returns list of transactions", extensions = { @Extension(name = "translation", properties = { @@ -121,7 +129,9 @@ public class TransactionsResource { responses = { @ApiResponse( description = "list of transactions", - content = @Content(array = @ArraySchema(schema = @Schema(implementation = TransactionData.class))), + content = @Content(array = @ArraySchema(schema = @Schema( + oneOf = { GenesisTransactionData.class, PaymentTransactionData.class } + ))), extensions = { @Extension(name = "translation", properties = { @ExtensionProperty(name="description.key", value="success_response:description") @@ -130,9 +140,7 @@ public class TransactionsResource { ) } ) - public List<TransactionData> getBlockTransactions(@PathParam("signature") String signature) { - Security.checkApiCallAllowed("GET transactions/block", request); - + public List<TransactionData> getBlockTransactions(@PathParam("signature") String signature, @Parameter(ref = "limit") @QueryParam("limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) { // decode signature byte[] signatureBytes; try { @@ -148,13 +156,17 @@ public class TransactionsResource { if(transactions == null) throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS); + // Pagination would take effect here (or as part of the repository access) + int fromIndex = Integer.min(offset, transactions.size()); + int toIndex = limit == 0 ? transactions.size() : Integer.min(fromIndex + limit, transactions.size()); + transactions = transactions.subList(fromIndex, toIndex); + return transactions; } catch (ApiException e) { throw e; } catch (DataException e) { throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e); } - } } diff --git a/src/api/UtilsResource.java b/src/api/UtilsResource.java new file mode 100644 index 00000000..ba77a707 --- /dev/null +++ b/src/api/UtilsResource.java @@ -0,0 +1,66 @@ +package api; + +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.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import utils.Base58; + +import java.util.Base64; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; + +@Path("/utils") +@Produces({MediaType.TEXT_PLAIN}) +@Tag(name = "utils") +public class UtilsResource { + + @Context + HttpServletRequest request; + + @GET + @Path("/base58from64/{base64}") + @Operation( + summary = "Convert base64 data to base58", + responses = { + @ApiResponse( + description = "base58 data", + content = @Content(schema = @Schema(implementation = String.class)) + ) + } + ) + public String base58from64(@PathParam("base64") String base64) { + try { + return Base58.encode(Base64.getDecoder().decode(base64)); + } catch (IllegalArgumentException e) { + throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_DATA); + } + } + + @GET + @Path("/base64from58/{base58}") + @Operation( + summary = "Convert base58 data to base64", + responses = { + @ApiResponse( + description = "base64 data", + content = @Content(schema = @Schema(implementation = String.class)) + ) + } + ) + public String base64from58(@PathParam("base58") String base58) { + try { + return Base64.getEncoder().encodeToString(Base58.decode(base58)); + } catch (NumberFormatException e) { + throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_DATA); + } + } + +} diff --git a/src/api/models/BlockWithTransactions.java b/src/api/models/BlockWithTransactions.java new file mode 100644 index 00000000..ae323aa9 --- /dev/null +++ b/src/api/models/BlockWithTransactions.java @@ -0,0 +1,43 @@ +package api.models; + +import java.util.List; +import java.util.stream.Collectors; + +import javax.xml.bind.annotation.XmlElement; + +import api.ApiError; +import api.ApiErrorFactory; +import data.block.BlockData; +import data.transaction.TransactionData; +import io.swagger.v3.oas.annotations.media.Schema; +import qora.block.Block; +import repository.DataException; +import repository.Repository; + +@Schema(description = "Block with (optional) transactions") +public class BlockWithTransactions { + + @Schema(implementation = BlockData.class, name = "block", title = "block data") + @XmlElement(name = "block") + public BlockData blockData; + + public List<TransactionData> transactions; + + // For JAX-RS + @SuppressWarnings("unused") + private BlockWithTransactions() { + } + + public BlockWithTransactions(Repository repository, BlockData blockData, boolean includeTransactions) throws DataException { + if (blockData == null) + throw ApiErrorFactory.getInstance().createError(ApiError.BLOCK_NO_EXISTS); + + this.blockData = blockData; + + if (includeTransactions) { + Block block = new Block(repository, blockData); + this.transactions = block.getTransactions().stream().map(transaction -> transaction.getTransactionData()).collect(Collectors.toList()); + } + } + +} diff --git a/src/controller/Controller.java b/src/controller/Controller.java index 37c01250..460e7368 100644 --- a/src/controller/Controller.java +++ b/src/controller/Controller.java @@ -14,19 +14,31 @@ public class Controller { private static final Logger LOGGER = LogManager.getLogger(Controller.class); private static final String connectionUrl = "jdbc:hsqldb:file:db/test;create=true"; + + public static final long startTime = System.currentTimeMillis(); private static final Object shutdownLock = new Object(); private static boolean isStopping = false; - public static void main(String args[]) throws DataException { + public static void main(String args[]) { LOGGER.info("Starting up..."); LOGGER.info("Starting repository"); - RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(connectionUrl); - RepositoryManager.setRepositoryFactory(repositoryFactory); + try { + RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(connectionUrl); + RepositoryManager.setRepositoryFactory(repositoryFactory); + } catch (DataException e) { + LOGGER.error("Unable to start repository", e); + System.exit(1); + } LOGGER.info("Starting API"); - ApiService apiService = ApiService.getInstance(); - apiService.start(); + try { + ApiService apiService = ApiService.getInstance(); + apiService.start(); + } catch (Exception e) { + LOGGER.error("Unable to start API", e); + System.exit(1); + } Runtime.getRuntime().addShutdownHook(new Thread() { @Override diff --git a/src/data/block/BlockData.java b/src/data/block/BlockData.java index 1af4803e..4b794426 100644 --- a/src/data/block/BlockData.java +++ b/src/data/block/BlockData.java @@ -29,8 +29,7 @@ public class BlockData implements Serializable { private BigDecimal atFees; // necessary for JAX-RS serialization - @SuppressWarnings("unused") - private BlockData() { + protected BlockData() { } public BlockData(int version, byte[] reference, int transactionCount, BigDecimal totalFees, byte[] transactionsSignature, Integer height, long timestamp, diff --git a/src/data/transaction/GenesisTransactionData.java b/src/data/transaction/GenesisTransactionData.java index 06393825..f30561b6 100644 --- a/src/data/transaction/GenesisTransactionData.java +++ b/src/data/transaction/GenesisTransactionData.java @@ -2,9 +2,16 @@ package data.transaction; import java.math.BigDecimal; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; + +import io.swagger.v3.oas.annotations.media.Schema; import qora.account.GenesisAccount; import qora.transaction.Transaction.TransactionType; +// All properties to be converted to JSON via JAX-RS +@XmlAccessorType(XmlAccessType.FIELD) +@Schema( allOf = { TransactionData.class } ) public class GenesisTransactionData extends TransactionData { // Properties @@ -13,6 +20,10 @@ public class GenesisTransactionData extends TransactionData { // Constructors + // For JAX-RS + protected GenesisTransactionData() { + } + public GenesisTransactionData(String recipient, BigDecimal amount, long timestamp, byte[] signature) { // Zero fee super(TransactionType.GENESIS, BigDecimal.ZERO, GenesisAccount.PUBLIC_KEY, timestamp, null, signature); diff --git a/src/data/transaction/PaymentTransactionData.java b/src/data/transaction/PaymentTransactionData.java index 98afb3ab..e93ada3a 100644 --- a/src/data/transaction/PaymentTransactionData.java +++ b/src/data/transaction/PaymentTransactionData.java @@ -2,8 +2,15 @@ package data.transaction; import java.math.BigDecimal; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; + +import io.swagger.v3.oas.annotations.media.Schema; import qora.transaction.Transaction.TransactionType; +// All properties to be converted to JSON via JAX-RS +@XmlAccessorType(XmlAccessType.FIELD) +@Schema( allOf = { TransactionData.class } ) public class PaymentTransactionData extends TransactionData { // Properties @@ -13,6 +20,10 @@ public class PaymentTransactionData extends TransactionData { // Constructors + // For JAX-RS + protected PaymentTransactionData() { + } + public PaymentTransactionData(byte[] senderPublicKey, String recipient, BigDecimal amount, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) { super(TransactionType.PAYMENT, fee, senderPublicKey, timestamp, reference, signature); diff --git a/src/data/transaction/TransactionData.java b/src/data/transaction/TransactionData.java index 428223c5..820d54c8 100644 --- a/src/data/transaction/TransactionData.java +++ b/src/data/transaction/TransactionData.java @@ -23,6 +23,10 @@ public abstract class TransactionData { // Constructors + // For JAX-RS + protected TransactionData() { + } + public TransactionData(TransactionType type, BigDecimal fee, byte[] creatorPublicKey, long timestamp, byte[] reference, byte[] signature) { this.fee = fee; this.type = type; diff --git a/src/qora/account/Account.java b/src/qora/account/Account.java index 23053a31..78dca2eb 100644 --- a/src/qora/account/Account.java +++ b/src/qora/account/Account.java @@ -1,7 +1,6 @@ package qora.account; import java.math.BigDecimal; -import java.util.Arrays; import java.util.List; import org.apache.logging.log4j.LogManager; diff --git a/src/repository/hsqldb/HSQLDBRepositoryFactory.java b/src/repository/hsqldb/HSQLDBRepositoryFactory.java index b5e368bc..ed35a067 100644 --- a/src/repository/hsqldb/HSQLDBRepositoryFactory.java +++ b/src/repository/hsqldb/HSQLDBRepositoryFactory.java @@ -20,6 +20,12 @@ public class HSQLDBRepositoryFactory implements RepositoryFactory { // one-time initialization goes in here this.connectionUrl = connectionUrl; + // Check no-one else is accessing database + try (Connection connection = DriverManager.getConnection(this.connectionUrl)) { + } catch (SQLException e) { + throw new DataException("Unable to open repository: " + e.getMessage()); + } + this.connectionPool = new JDBCPool(); this.connectionPool.setUrl(this.connectionUrl);