From bb852b48eef2a1ab012e728fc9d06dddfba0539e Mon Sep 17 00:00:00 2001 From: Patrick Jusic Date: Wed, 20 Mar 2019 11:36:33 +0100 Subject: [PATCH 1/3] Features: Allow IXI Modules to dictate response's content-type. (#743) Updates old feature pull request improving X-IOTA-API-Version check management --- src/main/java/com/iota/iri/service/API.java | 88 +++++++++++++++---- .../com/iota/iri/service/dto/IXIResponse.java | 24 +++++ 2 files changed, 95 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/iota/iri/service/API.java b/src/main/java/com/iota/iri/service/API.java index 9f345899ae..d10fe4add7 100644 --- a/src/main/java/com/iota/iri/service/API.java +++ b/src/main/java/com/iota/iri/service/API.java @@ -40,6 +40,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.IOException; @@ -88,7 +89,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); @@ -220,7 +221,6 @@ public void handleRequest(final HttpServerExchange exchange) throws Exception { */ private void sendResponse(HttpServerExchange exchange, AbstractResponse res, long beginningTime) throws IOException { res.setDuration((int) (System.currentTimeMillis() - beginningTime)); - final String response = gson.toJson(res); if (res instanceof ErrorResponse) { // bad request or invalid parameters @@ -233,7 +233,10 @@ private void sendResponse(HttpServerExchange exchange, AbstractResponse res, lon exchange.setStatusCode(500); } - setupResponseHeaders(exchange); + + setupResponseHeaders(exchange, res); + + final String response = convertResponseToClientFormat(res); ByteBuffer responseBuf = ByteBuffer.wrap(response.getBytes(StandardCharsets.UTF_8)); exchange.setResponseContentLength(responseBuf.array().length); @@ -258,6 +261,21 @@ private void sendResponse(HttpServerExchange exchange, AbstractResponse res, lon 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; + } + /** *

* Processes an API HTTP request. @@ -275,23 +293,48 @@ private void sendResponse(HttpServerExchange exchange, AbstractResponse res, lon * @throws IOException If the body of this HTTP request cannot be read */ 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); - AbstractResponse response; - if (!exchange.getRequestHeaders().contains("X-IOTA-API-Version")) { - response = ErrorResponse.create("Invalid API Version"); - } else if (body.length() > maxBodyLength) { - response = ErrorResponse.create("Request too long"); - } else { - response = process(body, exchange.getSourceAddress()); + AbstractResponse response; + try { + final String body = getRequestBody(exchange); + if (body.length() > maxBodyLength) { + response = ErrorResponse.create("Request too long"); + } else { + response = process(body, exchange); + } + } catch (IOException e) { + log.error("API Exception: {}", e.getLocalizedMessage(), e); + response = ErrorResponse.create(e.getLocalizedMessage()); } sendResponse(exchange, response, beginningTime); } + 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()); + } else if (!exchange.getRequestHeaders().contains("X-IOTA-API-Version")) { + throw new IOException ("Invalid API Version"); + } + 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); + } + /** * Handles an API request body. * Its returned {@link AbstractResponse} is created using the following logic @@ -324,8 +367,7 @@ private void processRequest(final HttpServerExchange exchange) throws IOExceptio * @throws UnsupportedEncodingException If the requestString cannot be parsed into a Map. Currently caught and turned into a {@link ExceptionResponse}. */ - private AbstractResponse process(final String requestString, InetSocketAddress sourceAddress) - throws UnsupportedEncodingException { + private AbstractResponse process(final String requestString, final HttpServerExchange exchange) throws UnsupportedEncodingException { try { // Request JSON data into map @@ -346,6 +388,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(); // Is this command allowed to be run from this request address? // We check the remote limit API configuration. if (instance.configuration.getRemoteLimitApi().contains(command) && @@ -1624,12 +1667,23 @@ private boolean validTrytes(String trytes, int length, char zeroAllowed) { * Updates the {@link HttpServerExchange} {@link HeaderMap} with the proper response settings. * @param exchange Contains information about what the client has send to us */ - private static void setupResponseHeaders(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"; + } + } + /** * Sets up the {@link HttpHandler} to have correct security settings. * Remote authentication is blocked for anyone except 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 2bd32f4f03..5f9a588706 100644 --- a/src/main/java/com/iota/iri/service/dto/IXIResponse.java +++ b/src/main/java/com/iota/iri/service/dto/IXIResponse.java @@ -1,6 +1,8 @@ package com.iota.iri.service.dto; import com.iota.iri.IXI; +import java.util.Map; +import org.apache.commons.lang3.StringUtils; /** *

@@ -27,4 +29,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; + } } From c51844961eecf5e755bdf15b8290ea9e5a705cac Mon Sep 17 00:00:00 2001 From: Patrick Jusic Date: Wed, 20 Mar 2019 12:18:53 +0100 Subject: [PATCH 2/3] Fix: adds missing and fixes old comments to API.java --- src/main/java/com/iota/iri/service/API.java | 47 ++++++++++++++++--- .../com/iota/iri/service/dto/IXIResponse.java | 6 +-- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/iota/iri/service/API.java b/src/main/java/com/iota/iri/service/API.java index d10fe4add7..dcbceccb9b 100644 --- a/src/main/java/com/iota/iri/service/API.java +++ b/src/main/java/com/iota/iri/service/API.java @@ -261,6 +261,17 @@ private void sendResponse(HttpServerExchange exchange, AbstractResponse res, lon sinkChannel.resumeWrites(); } + /** + *

+ * Converts the abstract response to String based on type of response. + * Extracts the content the res if it is an instance of IXIResponse in order + * to serve it otherwise returnes the response as JSON string. + *

+ * + * @param res The response of the API. + * See {@link #processRequest(HttpServerExchange)} + * and {@link #process(String, InetSocketAddress)} for the different responses in each case. + */ private String convertResponseToClientFormat(AbstractResponse res) { String response = null; if(res instanceof IXIResponse){ @@ -289,8 +300,8 @@ private String convertResponseToClientFormat(AbstractResponse res) { * The result is sent back to the requester. *

* - * @param exchange Contains the data the client sent to us - * @throws IOException If the body of this HTTP request cannot be read + * @param exchange Contains the data the client sent to us. + * @throws IOException If the body of this HTTP request cannot be read. */ private void processRequest(final HttpServerExchange exchange) throws IOException { final long beginningTime = System.currentTimeMillis(); @@ -310,6 +321,18 @@ private void processRequest(final HttpServerExchange exchange) throws IOExceptio sendResponse(exchange, response, beginningTime); } + /** + *

+ * Extracts a json body based on type of HTTP request. + * In case of POST request the body is taken from the request body and the request + * should contain of X-IOTA-API-Version header, otherwise, an exception is raised. + * In another case, the body is extracted with getQueryParamsBody to build up a json + * based on query parameters. + *

+ * + * @param exchange Contains the data the client sent to us. + * @throws IOException If the body of this HTTP request cannot be read. + */ private String getRequestBody(final HttpServerExchange exchange) throws IOException { StreamSourceChannel requestChannel = exchange.getRequestChannel(); final ChannelInputStream cis = new ChannelInputStream(requestChannel); @@ -323,16 +346,24 @@ private String getRequestBody(final HttpServerExchange exchange) throws IOExcept return body; } - private String getQueryParamsBody(Map> queryParameters) { + /** + *

+ * Extracts query parameters and builds up a json object using them + * as key value. + *

+ * + * @param queryParameters Contains a mutable map of query parameters. + */ + private String getQueryParamsBody(Map> queryParameters) { Map parametersMapper = new HashMap(); - for (String key : queryParameters.keySet()) { + for (String key : queryParameters.keySet()) { Deque dequeuedParameter = queryParameters.get(key); String parameterValue = dequeuedParameter.getFirst(); parametersMapper.put(key, parameterValue); } - return gson.toJson(parametersMapper); + return gson.toJson(parametersMapper); } /** @@ -362,7 +393,7 @@ private String getQueryParamsBody(Map> queryParameters) { * * @param requestString The JSON encoded data of the request. * This String is attempted to be converted into a {@code Map}. - * @param sourceAddress The address from the sender of this API request. + * @param exchange Contains the data the client sent to us. * @return The result of this request. * @throws UnsupportedEncodingException If the requestString cannot be parsed into a Map. Currently caught and turned into a {@link ExceptionResponse}. @@ -1666,7 +1697,9 @@ private boolean validTrytes(String trytes, int length, char zeroAllowed) { /** * Updates the {@link HttpServerExchange} {@link HeaderMap} with the proper response settings. * @param exchange Contains information about what the client has send to us - */ + * @param res The response of the API. + * See {@link #processRequest(HttpServerExchange)} + * and {@link #process(String, InetSocketAddress)} for the different responses in each case. */ private static void setupResponseHeaders(final HttpServerExchange exchange, final AbstractResponse res) { final HeaderMap headerMap = exchange.getResponseHeaders(); headerMap.add(new HttpString("Access-Control-Allow-Origin"),"*"); 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 5f9a588706..df7e37a2e3 100644 --- a/src/main/java/com/iota/iri/service/dto/IXIResponse.java +++ b/src/main/java/com/iota/iri/service/dto/IXIResponse.java @@ -34,18 +34,18 @@ private String getdefaultContentType() { return "application/json"; } - public String getResponseContentType() { + public String getResponseContentType() { Map responseMapper = getResponseMapper(); String fieldObj = (String)responseMapper.get("contentType"); String fieldValue = StringUtils.isBlank(fieldObj) ? getdefaultContentType() : fieldObj; return fieldValue; } - private Map getResponseMapper(){ + private Map getResponseMapper(){ return (Map)ixi; } - public String getContent() { + public String getContent() { Map responseMapper = getResponseMapper(); String fieldObj = (String)responseMapper.get("content"); String fieldValue = StringUtils.isBlank(fieldObj) ? null : fieldObj; From 1a870ceac732c098fb864fc85149fb20954bcf7e Mon Sep 17 00:00:00 2001 From: Patrick Jusic Date: Wed, 20 Mar 2019 17:01:10 +0100 Subject: [PATCH 3/3] Fix: adds missing comments on functions in IXIResponse --- .../com/iota/iri/service/dto/IXIResponse.java | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) 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 df7e37a2e3..8967785572 100644 --- a/src/main/java/com/iota/iri/service/dto/IXIResponse.java +++ b/src/main/java/com/iota/iri/service/dto/IXIResponse.java @@ -30,21 +30,33 @@ public Object getResponse() { return ixi; } - private String getdefaultContentType() { + /** + * Returnes "application/json" as the default content type of the API response. + */ + private String getDefaultContentType() { return "application/json"; } - + + /** + * Returnes the contentType in the contentType field of ixi, otherwise the default contentType. + */ public String getResponseContentType() { Map responseMapper = getResponseMapper(); String fieldObj = (String)responseMapper.get("contentType"); - String fieldValue = StringUtils.isBlank(fieldObj) ? getdefaultContentType() : fieldObj; + String fieldValue = StringUtils.isBlank(fieldObj) ? getDefaultContentType() : fieldObj; return fieldValue; } + /** + * Returnes the casted version of ixi to a Map instance. + */ private Map getResponseMapper(){ return (Map)ixi; } + /** + * Returnes the string in the content field of ixi, otherwise null if the field is empty. + */ public String getContent() { Map responseMapper = getResponseMapper(); String fieldObj = (String)responseMapper.get("content");