-
Notifications
You must be signed in to change notification settings - Fork 370
Allow IXI Modules to dictate response's content-type. #743
base: dev
Are you sure you want to change the base?
Changes from 6 commits
86a5189
09d7a11
ad0b1d2
ecafa1f
43049df
08ab8d5
5bab03b
58be2c5
716214d
c0a23a2
4f8551f
c4094f8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
@@ -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); | ||
|
@@ -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(); | ||
queryParamsBody += "\"" + key + "\" : " + "\"" + param + "\","; // Json property | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We are still using Java 8, There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
|
||
|
@@ -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()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. concatenated gets... There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)){ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this always appear? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: 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"); | ||
} | ||
|
@@ -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"); | ||
|
@@ -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): | ||
|
@@ -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."); | ||
|
@@ -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(), | ||
|
@@ -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."); | ||
|
@@ -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); | ||
|
@@ -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()) | ||
|
@@ -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."); | ||
|
@@ -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); | ||
} | ||
|
@@ -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()]; | ||
|
@@ -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) { | ||
|
@@ -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); | ||
|
@@ -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("")){ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should Use |
||
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 +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()) { | ||
|
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. | ||
*/ | ||
|
@@ -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"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. concatenated gets |
||
String fieldValue = fieldObj == null || "".equals(fieldObj) ? getdefaultContentType() : fieldObj.toString(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should Use |
||
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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should Use |
||
return fieldValue; | ||
} | ||
} |
There was a problem hiding this comment.
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?