diff --git a/src/main/java/com/iota/iri/service/API.java b/src/main/java/com/iota/iri/service/API.java index e9869dbad1..18bd2e4bfd 100644 --- a/src/main/java/com/iota/iri/service/API.java +++ b/src/main/java/com/iota/iri/service/API.java @@ -35,6 +35,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xnio.channels.StreamSinkChannel; +import org.xnio.channels.StreamSourceChannel; import org.xnio.streams.ChannelInputStream; import java.io.*; @@ -64,7 +65,7 @@ public class API { private Undertow server; - private final Gson gson = new GsonBuilder().create(); + private final Gson gson = new GsonBuilder().disableHtmlEscaping().create(); private volatile PearlDiver pearlDiver = new PearlDiver(); private final AtomicInteger counter = new AtomicInteger(0); @@ -169,24 +170,44 @@ private void readPreviousEpochsSpentAddresses(boolean isTestnet) throws IOExcept } private void processRequest(final HttpServerExchange exchange) throws IOException { - final ChannelInputStream cis = new ChannelInputStream(exchange.getRequestChannel()); - exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "application/json"); - final long beginningTime = System.currentTimeMillis(); - final String body = IotaIOUtils.toString(cis, StandardCharsets.UTF_8); + + final String body = getRequestBody(exchange); final AbstractResponse response; - if (!exchange.getRequestHeaders().contains("X-IOTA-API-Version")) { - response = ErrorResponse.create("Invalid API Version"); - } else if (body.length() > maxBodyLength) { + if (body.length() > maxBodyLength) { response = ErrorResponse.create("Request too long"); } else { - response = process(body, exchange.getSourceAddress()); + response = process(body, exchange); } + sendResponse(exchange, response, beginningTime); } - private AbstractResponse process(final String requestString, InetSocketAddress sourceAddress) throws UnsupportedEncodingException { + private String getRequestBody(final HttpServerExchange exchange) throws IOException { + StreamSourceChannel requestChannel = exchange.getRequestChannel(); + final ChannelInputStream cis = new ChannelInputStream(requestChannel); + String body = IotaIOUtils.toString(cis, StandardCharsets.UTF_8); + + if(body.length() == 0){ + body = getQueryParamsBody(exchange.getQueryParameters()); + } + return body; + } + + private String getQueryParamsBody(Map> queryParameters) { + Map parametersMapper = new HashMap(); + + for (String key : queryParameters.keySet()) { + Deque dequeuedParameter = queryParameters.get(key); + String parameterValue = dequeuedParameter.getFirst(); + parametersMapper.put(key, parameterValue); + } + + return gson.toJson(parametersMapper); + } + + private AbstractResponse process(final String requestString, final HttpServerExchange exchange) throws UnsupportedEncodingException { try { @@ -200,6 +221,7 @@ private AbstractResponse process(final String requestString, InetSocketAddress s return ErrorResponse.create("COMMAND parameter has not been specified in the request."); } + InetSocketAddress sourceAddress = exchange.getSourceAddress(); if (instance.configuration.string(DefaultConfSettings.REMOTE_LIMIT_API).contains(command) && !sourceAddress.getAddress().isLoopbackAddress()) { return AccessLimitedResponse.create("COMMAND " + command + " is not available on this node"); @@ -209,6 +231,10 @@ private AbstractResponse process(final String requestString, InetSocketAddress s switch (command) { case "storeMessage": { + if(!iotaAPIHeaderDefined(exchange)){ + return ErrorResponse.create("Invalid API Version"); + } + if (!testNet) { return AccessLimitedResponse.create("COMMAND storeMessage is only available on testnet"); } @@ -230,11 +256,19 @@ private AbstractResponse process(final String requestString, InetSocketAddress s } case "addNeighbors": { + if(!iotaAPIHeaderDefined(exchange)){ + return ErrorResponse.create("Invalid API Version"); + } + List uris = getParameterAsList(request,"uris",0); log.debug("Invoking 'addNeighbors' with {}", uris); return addNeighborsStatement(uris); } case "attachToTangle": { + if(!iotaAPIHeaderDefined(exchange)){ + return ErrorResponse.create("Invalid API Version"); + } + final Hash trunkTransaction = new Hash(getParameterAsStringAndValidate(request,"trunkTransaction", HASH_SIZE)); final Hash branchTransaction = new Hash(getParameterAsStringAndValidate(request,"branchTransaction", HASH_SIZE)); final int minWeightMagnitude = getParameterAsInt(request,"minWeightMagnitude"); @@ -245,14 +279,26 @@ private AbstractResponse process(final String requestString, InetSocketAddress s return AttachToTangleResponse.create(elements); } case "broadcastTransactions": { + if(!iotaAPIHeaderDefined(exchange)){ + return ErrorResponse.create("Invalid API Version"); + } + final List trytes = getParameterAsList(request,"trytes", TRYTES_SIZE); broadcastTransactionStatement(trytes); return AbstractResponse.createEmptyResponse(); } case "findTransactions": { + if(!iotaAPIHeaderDefined(exchange)){ + return ErrorResponse.create("Invalid API Version"); + } + return findTransactionStatement(request); } case "getBalances": { + if(!iotaAPIHeaderDefined(exchange)){ + return ErrorResponse.create("Invalid API Version"); + } + final List addresses = getParameterAsList(request,"addresses", HASH_SIZE); final List tips = request.containsKey("tips") ? getParameterAsList(request,"tips ", HASH_SIZE): @@ -261,6 +307,10 @@ private AbstractResponse process(final String requestString, InetSocketAddress s return getBalancesStatement(addresses, tips, threshold); } case "getInclusionStates": { + if(!iotaAPIHeaderDefined(exchange)){ + return ErrorResponse.create("Invalid API Version"); + } + if (invalidSubtangleStatus()) { return ErrorResponse .create("This operations cannot be executed: The subtangle has not been updated yet."); @@ -271,9 +321,17 @@ private AbstractResponse process(final String requestString, InetSocketAddress s return getNewInclusionStateStatement(transactions, tips); } case "getNeighbors": { + if(!iotaAPIHeaderDefined(exchange)){ + return ErrorResponse.create("Invalid API Version"); + } + return getNeighborsStatement(); } case "getNodeInfo": { + if(!iotaAPIHeaderDefined(exchange)){ + return ErrorResponse.create("Invalid API Version"); + } + String name = instance.configuration.booling(Configuration.DefaultConfSettings.TESTNET) ? IRI.TESTNET_NAME : IRI.MAINNET_NAME; return GetNodeInfoResponse.create(name, IRI.VERSION, Runtime.getRuntime().availableProcessors(), Runtime.getRuntime().freeMemory(), System.getProperty("java.version"), Runtime.getRuntime().maxMemory(), @@ -284,9 +342,17 @@ private AbstractResponse process(final String requestString, InetSocketAddress s instance.transactionRequester.numberOfTransactionsToRequest()); } case "getTips": { + if(!iotaAPIHeaderDefined(exchange)){ + return ErrorResponse.create("Invalid API Version"); + } + return getTipsStatement(); } case "getTransactionsToApprove": { + if(!iotaAPIHeaderDefined(exchange)){ + return ErrorResponse.create("Invalid API Version"); + } + if (invalidSubtangleStatus()) { return ErrorResponse .create("This operations cannot be executed: The subtangle has not been updated yet."); @@ -313,21 +379,37 @@ private AbstractResponse process(final String requestString, InetSocketAddress s } } case "getTrytes": { + if(!iotaAPIHeaderDefined(exchange)){ + return ErrorResponse.create("Invalid API Version"); + } + final List hashes = getParameterAsList(request,"hashes", HASH_SIZE); return getTrytesStatement(hashes); } case "interruptAttachingToTangle": { + if(!iotaAPIHeaderDefined(exchange)){ + return ErrorResponse.create("Invalid API Version"); + } + pearlDiver.cancel(); return AbstractResponse.createEmptyResponse(); } case "removeNeighbors": { + if(!iotaAPIHeaderDefined(exchange)){ + return ErrorResponse.create("Invalid API Version"); + } + List uris = getParameterAsList(request,"uris",0); log.debug("Invoking 'removeNeighbors' with {}", uris); return removeNeighborsStatement(uris); } case "storeTransactions": { + if(!iotaAPIHeaderDefined(exchange)){ + return ErrorResponse.create("Invalid API Version"); + } + try { final List trytes = getParameterAsList(request,"trytes", TRYTES_SIZE); storeTransactionStatement(trytes); @@ -338,6 +420,10 @@ private AbstractResponse process(final String requestString, InetSocketAddress s } } case "getMissingTransactions": { + if(!iotaAPIHeaderDefined(exchange)){ + return ErrorResponse.create("Invalid API Version"); + } + //TransactionRequester.instance().rescanTransactionsToRequest(); synchronized (instance.transactionRequester) { List missingTx = Arrays.stream(instance.transactionRequester.getRequestedTransactions()) @@ -347,6 +433,10 @@ private AbstractResponse process(final String requestString, InetSocketAddress s } } case "checkConsistency": { + if(!iotaAPIHeaderDefined(exchange)){ + return ErrorResponse.create("Invalid API Version"); + } + if (invalidSubtangleStatus()) { return ErrorResponse .create("This operations cannot be executed: The subtangle has not been updated yet."); @@ -355,6 +445,10 @@ private AbstractResponse process(final String requestString, InetSocketAddress s return checkConsistencyStatement(transactions); } case "wereAddressesSpentFrom": { + if(!iotaAPIHeaderDefined(exchange)){ + return ErrorResponse.create("Invalid API Version"); + } + final List addresses = getParameterAsList(request,"addresses", HASH_SIZE); return wereAddressesSpentFromStatement(addresses); } @@ -375,6 +469,10 @@ private AbstractResponse process(final String requestString, InetSocketAddress s } } + private boolean iotaAPIHeaderDefined(final HttpServerExchange exchange) { + return exchange.getRequestHeaders().contains("X-IOTA-API-Version"); + } + private AbstractResponse wereAddressesSpentFromStatement(List addressesStr) throws Exception { final List addresses = addressesStr.stream().map(Hash::new).collect(Collectors.toList()); final boolean[] states = new boolean[addresses.size()]; @@ -1034,8 +1132,7 @@ private AbstractResponse addNeighborsStatement(final List uris) { private void sendResponse(final HttpServerExchange exchange, final AbstractResponse res, final long beginningTime) throws IOException { res.setDuration((int) (System.currentTimeMillis() - beginningTime)); - final String response = gson.toJson(res); - + if (res instanceof ErrorResponse) { exchange.setStatusCode(400); // bad request } else if (res instanceof AccessLimitedResponse) { @@ -1044,7 +1141,9 @@ private void sendResponse(final HttpServerExchange exchange, final AbstractRespo exchange.setStatusCode(500); // internal error } - setupResponseHeaders(exchange); + setupResponseHeaders(exchange, res); + + final String response = convertResponseToClientFormat(res); ByteBuffer responseBuf = ByteBuffer.wrap(response.getBytes(StandardCharsets.UTF_8)); exchange.setResponseContentLength(responseBuf.array().length); @@ -1069,6 +1168,21 @@ private void sendResponse(final HttpServerExchange exchange, final AbstractRespo sinkChannel.resumeWrites(); } + private String convertResponseToClientFormat(AbstractResponse res) { + String response = null; + if(res instanceof IXIResponse){ + final String content = ((IXIResponse)res).getContent(); + if(content != null && StringUtils.isNotBlank(content)){ + response = content; + } + } + if(response == null){ + response = gson.toJson(res); + } + + return response; + } + private boolean validTrytes(String trytes, int length, char zeroAllowed) { if (trytes.length() == 0 && zeroAllowed == ZERO_LENGTH_ALLOWED) { return true; @@ -1080,12 +1194,22 @@ private boolean validTrytes(String trytes, int length, char zeroAllowed) { return matcher.matches(); } - private static void setupResponseHeaders(final HttpServerExchange exchange) { + private static void setupResponseHeaders(final HttpServerExchange exchange, final AbstractResponse res) { final HeaderMap headerMap = exchange.getResponseHeaders(); headerMap.add(new HttpString("Access-Control-Allow-Origin"),"*"); headerMap.add(new HttpString("Keep-Alive"), "timeout=500, max=100"); + headerMap.put(Headers.CONTENT_TYPE, getResponseContentType(res)); } + private static String getResponseContentType(AbstractResponse response) { + if(response instanceof IXIResponse){ + return ((IXIResponse)response).getResponseContentType(); + } + else { + return "application/json"; + } + } + private HttpHandler addSecurity(final HttpHandler toWrap) { String credentials = instance.configuration.string(DefaultConfSettings.REMOTE_AUTH); if (credentials == null || credentials.isEmpty()) { diff --git a/src/main/java/com/iota/iri/service/dto/IXIResponse.java b/src/main/java/com/iota/iri/service/dto/IXIResponse.java index 9c40d0d5b6..01322883a4 100644 --- a/src/main/java/com/iota/iri/service/dto/IXIResponse.java +++ b/src/main/java/com/iota/iri/service/dto/IXIResponse.java @@ -1,5 +1,8 @@ package com.iota.iri.service.dto; +import java.util.Map; +import org.apache.commons.lang3.StringUtils; + /** * Created by paul on 2/10/17. */ @@ -15,4 +18,26 @@ public static IXIResponse create(Object myixi) { public Object getResponse() { return ixi; } + + private String getdefaultContentType() { + return "application/json"; + } + + public String getResponseContentType() { + Map responseMapper = getResponseMapper(); + String fieldObj = (String)responseMapper.get("contentType"); + String fieldValue = StringUtils.isBlank(fieldObj) ? getdefaultContentType() : fieldObj; + return fieldValue; + } + + private Map getResponseMapper(){ + return (Map)ixi; + } + + public String getContent() { + Map responseMapper = getResponseMapper(); + String fieldObj = (String)responseMapper.get("content"); + String fieldValue = StringUtils.isBlank(fieldObj) ? null : fieldObj; + return fieldValue; + } }