Skip to content
This repository was archived by the owner on Aug 23, 2020. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 108 additions & 21 deletions src/main/java/com/iota/iri/service/API.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand All @@ -258,6 +261,32 @@ private void sendResponse(HttpServerExchange exchange, AbstractResponse res, lon
sinkChannel.resumeWrites();
}

/**
* <p>
* 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.
* </p>
*
* @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){
final String content = ((IXIResponse)res).getContent();
if(content != null && StringUtils.isNotBlank(content)){
response = content;
}
}
if(response == null){
response = gson.toJson(res);
}

return response;
}

/**
* <p>
* Processes an API HTTP request.
Expand All @@ -271,27 +300,72 @@ private void sendResponse(HttpServerExchange exchange, AbstractResponse res, lon
* The result is sent back to the requester.
* </p>
*
* @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 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);
}

/**
* <p>
* 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.
* </p>
*
* @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);
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;
}

/**
* <p>
* Extracts query parameters and builds up a json object using them
* as key value.
* </p>
*
* @param queryParameters Contains a mutable map of query parameters.
*/
private String getQueryParamsBody(Map<String, Deque<String>> queryParameters) {
Map<String, String> parametersMapper = new HashMap<String, String>();

for (String key : queryParameters.keySet()) {
Deque<String> 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
Expand Down Expand Up @@ -319,13 +393,12 @@ private void processRequest(final HttpServerExchange exchange) throws IOExceptio
*
* @param requestString The JSON encoded data of the request.
* This String is attempted to be converted into a {@code Map<String, Object>}.
* @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}.
*/
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
Expand All @@ -346,6 +419,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) &&
Expand Down Expand Up @@ -1623,13 +1697,26 @@ 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) {
* @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"),"*");
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
Expand Down
36 changes: 36 additions & 0 deletions src/main/java/com/iota/iri/service/dto/IXIResponse.java
Original file line number Diff line number Diff line change
@@ -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;

/**
* <p>
Expand All @@ -27,4 +29,38 @@ public static IXIResponse create(Object myixi) {
public Object getResponse() {
return ixi;
}

/**
* 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<String, Object> responseMapper = getResponseMapper();
String fieldObj = (String)responseMapper.get("contentType");
String fieldValue = StringUtils.isBlank(fieldObj) ? getDefaultContentType() : fieldObj;
return fieldValue;
}

/**
* Returnes the casted version of ixi to a Map<String, Object> instance.
*/
private Map<String, Object> getResponseMapper(){
return (Map<String, Object>)ixi;
}

/**
* Returnes the string in the content field of ixi, otherwise null if the field is empty.
*/
public String getContent() {
Map<String, Object> responseMapper = getResponseMapper();
String fieldObj = (String)responseMapper.get("content");
String fieldValue = StringUtils.isBlank(fieldObj) ? null : fieldObj;
return fieldValue;
}
}