From bb852b48eef2a1ab012e728fc9d06dddfba0539e Mon Sep 17 00:00:00 2001 From: Patrick Jusic Date: Wed, 20 Mar 2019 11:36:33 +0100 Subject: [PATCH] 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; + } }