Skip to content
This repository has been archived by the owner on Aug 23, 2020. It is now read-only.

Allow IXI Modules to dictate response's content-type. #743

Open
wants to merge 12 commits into
base: dev
Choose a base branch
from
158 changes: 142 additions & 16 deletions src/main/java/com/iota/iri/service/API.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
import org.xnio.streams.ChannelInputStream;

import java.io.*;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
Expand All @@ -64,7 +63,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 @@ -169,24 +168,49 @@ 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 = IOUtils.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 {
final ChannelInputStream cis = new ChannelInputStream(exchange.getRequestChannel());
String body = IOUtils.toString(cis, StandardCharsets.UTF_8);

if(body.length() == 0){
body = getQueryParamsBody(exchange.getQueryParameters());
}
return body;
}

private String getQueryParamsBody(Map<String, Deque<String>> queryParameters) {
String queryParamsBody = "{";

Set<String> keySet = queryParameters.keySet();

for (String key : keySet) {
String param = queryParameters.get(key).getFirst();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like concatenated gets...
Can you seperate to two lines?

queryParamsBody += "\"" + key + "\" : " + "\"" + param + "\","; // Json property
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are still using Java 8,
So can you use StringBuilder?

Copy link
Author

@brunoamancio brunoamancio May 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've used gson to convert the parameters. It looks cleaner now.

}

// Removes last comma, if multiple params
if(queryParamsBody.endsWith(",")){
queryParamsBody = queryParamsBody.substring(0, queryParamsBody.length() -1);
}

return queryParamsBody + "}";
}

private AbstractResponse process(final String requestString, final HttpServerExchange exchange) throws UnsupportedEncodingException {

try {

Expand All @@ -201,14 +225,18 @@ private AbstractResponse process(final String requestString, InetSocketAddress s
}

if (instance.configuration.string(DefaultConfSettings.REMOTE_LIMIT_API).contains(command) &&
!sourceAddress.getAddress().isLoopbackAddress()) {
!exchange.getSourceAddress().getAddress().isLoopbackAddress()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

concatenated gets...

Copy link
Author

@brunoamancio brunoamancio May 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will do that, although the extra variable will be set even when the first condition isn't met. That means we are trading performance for maintainability.

return AccessLimitedResponse.create("COMMAND " + command + " is not available on this node");
}

log.debug("# {} -> Requesting command '{}'", counter.incrementAndGet(), command);

switch (command) {
case "storeMessage": {
if(!iotaAPIHeaderDefined(exchange)){
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this always appear?
Instead of copy/pasting it can you write it once before the switch?

Copy link
Author

@brunoamancio brunoamancio May 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is to not require the API version header for calls to IXI modules. Due to the current use of a switch we have 3 options:
1 - Paste the check for every API call, except call to IXI modules (which is what I've done)
2 - Refactor the current logic to use command pattern
3 - Use a Map structure to map command names to API calls (kind of a command pattern)

I can refactor the existing command structure, but that should be done in another PR.

return ErrorResponse.create("Invalid API Version");
}

if (!testNet) {
return AccessLimitedResponse.create("COMMAND storeMessage is only available on testnet");
}
Expand All @@ -230,11 +258,19 @@ private AbstractResponse process(final String requestString, InetSocketAddress s
}

case "addNeighbors": {
if(!iotaAPIHeaderDefined(exchange)){
return ErrorResponse.create("Invalid API Version");
}

List<String> 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");
Expand All @@ -245,14 +281,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<String> 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<String> addresses = getParameterAsList(request,"addresses", HASH_SIZE);
final List<String> tips = request.containsKey("tips") ?
getParameterAsList(request,"tips ", HASH_SIZE):
Expand All @@ -261,6 +309,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.");
Expand All @@ -271,9 +323,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(),
Expand All @@ -284,9 +344,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.");
Expand All @@ -313,21 +381,37 @@ private AbstractResponse process(final String requestString, InetSocketAddress s
}
}
case "getTrytes": {
if(!iotaAPIHeaderDefined(exchange)){
return ErrorResponse.create("Invalid API Version");
}

final List<String> 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<String> 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<String> trytes = getParameterAsList(request,"trytes", TRYTES_SIZE);
storeTransactionStatement(trytes);
Expand All @@ -338,6 +422,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<String> missingTx = Arrays.stream(instance.transactionRequester.getRequestedTransactions())
Expand All @@ -347,6 +435,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.");
Expand All @@ -355,6 +447,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<String> addresses = getParameterAsList(request,"addresses", HASH_SIZE);
return wereAddressesSpentFromStatement(addresses);
}
Expand All @@ -375,6 +471,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<String> addressesStr) throws Exception {
final List<Hash> addresses = addressesStr.stream().map(Hash::new).collect(Collectors.toList());
final boolean[] states = new boolean[addresses.size()];
Expand Down Expand Up @@ -1034,8 +1134,7 @@ private AbstractResponse addNeighborsStatement(final List<String> 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) {
Expand All @@ -1044,7 +1143,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);
Expand All @@ -1069,6 +1170,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 && !content.equals("")){
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should " " pass?

Use StringUtils.isNotEmpty() or StringUtils.isNotBlank() according to your answer

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;
Expand All @@ -1080,12 +1196,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()) {
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/com/iota/iri/service/dto/IXIResponse.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.iota.iri.service.dto;

import java.util.Map;

/**
* Created by paul on 2/10/17.
*/
Expand All @@ -15,4 +17,24 @@ public static IXIResponse create(Object myixi) {
public Object getResponse() {
return ixi;
}

private String getdefaultContentType() {
return "application/json";
}

public String getResponseContentType() {
Object fieldObj = getResponseMapper().get("contentType");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

concatenated gets

String fieldValue = fieldObj == null || "".equals(fieldObj) ? getdefaultContentType() : fieldObj.toString();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should " " pass?

Use StringUtils.isNotEmpty() or StringUtils.isNotBlank() according to your answer

return fieldValue;
}

private Map<String, Object> getResponseMapper(){
return (Map<String, Object>)ixi;
}

public String getContent() {
Object fieldObj = getResponseMapper().get("content");
String fieldValue = fieldObj == null || "".equals(fieldObj) ? null : fieldObj.toString();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should " " pass?

Use StringUtils.isNotEmpty() or StringUtils.isNotBlank() according to your answer

return fieldValue;
}
}