Skip to content

Commit

Permalink
API: make all GET methods safe to public access
Browse files Browse the repository at this point in the history
  • Loading branch information
semux committed Sep 4, 2019
1 parent 30d20fe commit 6f91d0e
Show file tree
Hide file tree
Showing 20 changed files with 338 additions and 157 deletions.
8 changes: 6 additions & 2 deletions config/semux.properties
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,22 @@ net.dnsSeeds.testNet = testnet.semux.org
# API
#================

# If you enable API, please protect the API password below. Once authenticated,
# clients will gain full access to your wallet.
# Please update the password below before you enable API.
# NOTE: Once authenticated, clients get full access to your wallet.
api.enabled = false

# Listening address and port
api.listenIp = 127.0.0.1
api.listenPort = 5171

# Basic authentication
api.authEnabled = true
api.username = YOUR_API_USERNAME
api.password = YOUR_API_PASSWORD

# Allow GET methods (safe for public access) only
api.allowGetMethodsOnly = false

#================
# UI
#================
Expand Down
28 changes: 28 additions & 0 deletions src/main/java/org/semux/Kernel.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import org.semux.consensus.SemuxBft;
import org.semux.consensus.SemuxSync;
import org.semux.core.BftManager;
import org.semux.core.Block;
import org.semux.core.BlockHeader;
import org.semux.core.Blockchain;
import org.semux.core.BlockchainImpl;
import org.semux.core.Genesis;
Expand All @@ -44,6 +46,8 @@
import org.semux.net.PeerClient;
import org.semux.net.PeerServer;
import org.semux.util.Bytes;
import org.semux.util.TimeUtil;
import org.semux.vm.client.SemuxBlock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;
Expand Down Expand Up @@ -492,7 +496,31 @@ public PeerServer getP2p() {
return p2p;
}

/**
* Returns the database factory.
*
* @return
*/
public DatabaseFactory getDbFactory() {
return dbFactory;
}

/**
* Create an empty block.
*
* @return
*/
public SemuxBlock createEmptyBlock() {
Block prevBlock = getBlockchain().getLatestBlock();
BlockHeader blockHeader = new BlockHeader(
prevBlock.getNumber() + 1,
new Key().toAddress(),
prevBlock.getHash(),
TimeUtil.currentTimeMillis(),
Bytes.EMPTY_HASH,
Bytes.EMPTY_HASH,
Bytes.EMPTY_HASH,
Bytes.EMPTY_BYTES);
return new SemuxBlock(blockHeader, getConfig().spec().maxBlockGasLimit());
}
}
13 changes: 11 additions & 2 deletions src/main/java/org/semux/api/http/HttpHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import static io.netty.handler.codec.http.HttpHeaderValues.CLOSE;
import static io.netty.handler.codec.http.HttpHeaderValues.KEEP_ALIVE;
import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import static io.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN;
import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR;
import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;
import static io.netty.handler.codec.http.HttpResponseStatus.OK;
Expand Down Expand Up @@ -53,6 +54,7 @@
import io.netty.handler.codec.http.HttpChunkedInput;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.QueryStringDecoder;
Expand All @@ -69,6 +71,7 @@ public class HttpHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
public static final String INTERNAL_SERVER_ERROR_RESPONSE = "{\"success\":false,\"message\":\"500 Internal Server Error\"}";
public static final String NOT_FOUND_RESPONSE = "{\"success\":false,\"message\":\"404 Not Found\"}";
public static final String BAD_REQUEST_RESPONSE = "{\"success\":false,\"message\":\"400 Bad Request\"}";
public static final String FORBIDDEN_RESPONSE = "{\"success\":false,\"message\":\"403 Forbidden\"}";

private static final Charset CHARSET = CharsetUtil.UTF_8;
private static final ObjectMapper objectMapper = new ObjectMapper();
Expand Down Expand Up @@ -122,8 +125,8 @@ protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) {
HttpHeaders headers = msg.headers();
ByteBuf body = Unpooled.buffer(HttpConstants.MAX_BODY_SIZE);

// basic authentication
if (!checkBasicAuth(headers)) {
// check basic authentication
if (config.apiAuthEnabled() && !checkBasicAuth(headers)) {
FullHttpResponse resp = new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.UNAUTHORIZED);
resp.headers().set(HttpHeaderNames.WWW_AUTHENTICATE, "Basic realm=\"Semux RESTful API\"");
resp.headers().set(HttpHeaderNames.CONTENT_LENGTH, resp.content().readableBytes());
Expand All @@ -132,6 +135,12 @@ protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) {
return;
}

// check request method
if (config.apiAllowGetMethodsOnly() && !msg.method().equals(HttpMethod.GET)) {
writeJsonResponse(ctx, FORBIDDEN, FORBIDDEN_RESPONSE);
return;
}

// check decoding result
if (!msg.decoderResult().isSuccess()) {
writeJsonResponse(ctx, BAD_REQUEST, BAD_REQUEST_RESPONSE);
Expand Down
121 changes: 70 additions & 51 deletions src/main/java/org/semux/api/v2/SemuxApiImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,11 @@
import org.apache.commons.validator.routines.DomainValidator;
import org.apache.commons.validator.routines.InetAddressValidator;
import org.ethereum.vm.client.BlockStore;
import org.ethereum.vm.client.Repository;
import org.ethereum.vm.client.TransactionReceipt;
import org.ethereum.vm.program.invoke.ProgramInvokeFactoryImpl;
import org.ethereum.vm.util.HashUtil;
import org.semux.Kernel;
import org.semux.api.util.TransactionBuilder;
import org.semux.api.v2.model.AddNodeResponse;
import org.semux.api.v2.model.ApiHandlerResponse;
import org.semux.api.v2.model.CallResponse;
import org.semux.api.v2.model.ComposeRawTransactionResponse;
import org.semux.api.v2.model.CreateAccountResponse;
import org.semux.api.v2.model.DeleteAccountResponse;
Expand Down Expand Up @@ -61,22 +58,29 @@
import org.semux.api.v2.model.GetValidatorsResponse;
import org.semux.api.v2.model.GetVoteResponse;
import org.semux.api.v2.model.GetVotesResponse;
import org.semux.api.v2.model.LocalCallResponse;
import org.semux.api.v2.model.SignMessageResponse;
import org.semux.api.v2.model.SignRawTransactionResponse;
import org.semux.api.v2.model.SyncingStatusType;
import org.semux.api.v2.model.TransactionResultType;
import org.semux.api.v2.model.VerifyMessageResponse;
import org.semux.api.v2.server.SemuxApi;
import org.semux.config.Config;
import org.semux.core.Amount;
import org.semux.core.Block;
import org.semux.core.Blockchain;
import org.semux.core.BlockchainImpl;
import org.semux.core.PendingManager;
import org.semux.core.SyncManager;
import org.semux.core.Transaction;
import org.semux.core.TransactionExecutor;
import org.semux.core.TransactionResult;
import org.semux.core.TransactionType;
import org.semux.core.exception.WalletLockedException;
import org.semux.core.state.Account;
import org.semux.core.state.AccountState;
import org.semux.core.state.Delegate;
import org.semux.core.state.DelegateState;
import org.semux.crypto.CryptoException;
import org.semux.crypto.Hash;
import org.semux.crypto.Hex;
Expand All @@ -86,8 +90,6 @@
import org.semux.net.filter.SemuxIpFilter;
import org.semux.vm.client.SemuxBlock;
import org.semux.vm.client.SemuxBlockStore;
import org.semux.vm.client.SemuxRepository;
import org.semux.vm.client.SemuxTransaction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -530,7 +532,10 @@ public Response getTransactionResult(String hash) {
}

GetTransactionResultResponse resp = new GetTransactionResultResponse();
resp.setResult(TypeFactory.transactionResultType(tx, result, number));
byte[] contractAddress = (tx.getType() == TransactionType.CREATE)
? HashUtil.calcNewAddress(tx.getFrom(), tx.getNonce())
: null;
resp.setResult(TypeFactory.transactionResultType(result, tx.getFee(), contractAddress, number));
return success(resp);
} catch (NullPointerException | IllegalArgumentException e) {
return badRequest(e.getMessage());
Expand Down Expand Up @@ -695,29 +700,30 @@ public Response unvote(String from, String to, String value, String fee, String
}

@Override
public Response localCall(String to, String from, String value, String nonce, String data, String gas,
String gasPrice) {
TransactionReceipt receipt = doLocalCall(to, from, value, nonce, data, gas, gasPrice);
if (receipt == null || !receipt.isSuccess()) {
return badRequest("Failed to call locally");
} else {
CallResponse resp = new CallResponse();
resp.setResult(Hex.encode0x(receipt.getReturnData()));
return success(resp);
}
public Response localCall(String to, String value, String data, String gas, String gasPrice) {
TransactionResultType result = doLocalTransaction(TransactionType.CALL, to, value, data, gas, gasPrice);

LocalCallResponse resp = new LocalCallResponse();
resp.setResult(result);
return success(resp);
}

@Override
public Response estimateGas(String to, String from, String value, String nonce, String data, String gas,
String gasPrice) {
TransactionReceipt receipt = doLocalCall(to, from, value, nonce, data, gas, gasPrice);
if (receipt == null || !receipt.isSuccess()) {
return badRequest("Failed to estimate the gas usage");
} else {
EstimateGasResponse resp = new EstimateGasResponse();
resp.setResult(Long.toString(receipt.getGasUsed()));
return success(resp);
}
public Response localCreate(String value, String data, String gas, String gasPrice) {
TransactionResultType result = doLocalTransaction(TransactionType.CREATE, null, value, data, gas, gasPrice);

LocalCallResponse resp = new LocalCallResponse();
resp.setResult(result);
return success(resp);
}

@Override
public Response estimateGas(String to, String value, String data, String gas, String gasPrice) {
TransactionResultType result = doLocalTransaction(TransactionType.CALL, to, value, data, gas, gasPrice);

EstimateGasResponse resp = new EstimateGasResponse();
resp.setResult(result.getGasUsed());
return success(resp);
}

@Override
Expand Down Expand Up @@ -870,32 +876,45 @@ private Response doTransaction(TransactionType type, String from, String to, Str
}
}

private TransactionReceipt doLocalCall(String to, String from, String value, String nonce, String data, String gas,
String gasPrice) {

TransactionType type = TransactionType.CALL;
String fee = "0";
gas = (gas != null) ? gas : Long.toString(kernel.getConfig().spec().maxBlockGasLimit());
gasPrice = (gasPrice != null) ? gasPrice : "1";
from = (from != null) ? from : kernel.getCoinbase().toAddressString();
private TransactionResultType doLocalTransaction(TransactionType type, String to, String value, String data,
String gas, String gasPrice) {
long blockGasLimit = kernel.getConfig().spec().maxBlockGasLimit();
Amount minGasPrice = kernel.getConfig().poolMinGasPrice();

Transaction tx = getTransaction(type, from, to, value, fee, nonce, data, gas, gasPrice);
// NOTE: The only limitation now is that we can't specify the sender.

// simulated environment
SemuxTransaction transaction = new SemuxTransaction(tx);
SemuxBlock block = new SemuxBlock(kernel.getBlockchain().getLatestBlock().getHeader(),
kernel.getConfig().spec().maxBlockGasLimit());
Repository repository = new SemuxRepository(kernel.getBlockchain().getAccountState(),
kernel.getBlockchain().getDelegateState());
BlockStore blockStore = new SemuxBlockStore(kernel.getBlockchain());

// execute transaction
Repository track = repository.startTracking();
org.ethereum.vm.client.TransactionExecutor executor = new org.ethereum.vm.client.TransactionExecutor(
transaction, block, track, blockStore,
kernel.getConfig().spec().vmSpec(), new ProgramInvokeFactoryImpl(), 0);
// Setup the environment
Config config = kernel.getConfig();
Blockchain chain = kernel.getBlockchain();
AccountState asTrack = chain.getAccountState().track();
DelegateState dsTrack = chain.getDelegateState().track();

return executor.run();
// create a transaction signed by the coinbase account
Key sender = kernel.getCoinbase();
Transaction tx = new TransactionBuilder(kernel)
.withType(type)
.withFrom(sender.toAddressString())
.withTo(to)
.withValue(value)
.withData(data)
.withGas(gas != null ? gas : Long.toString(blockGasLimit))
.withGasPrice(gasPrice != null ? gasPrice : minGasPrice.toString())
.buildSigned();

// make sure the sender has enough balance
asTrack.adjustAvailable(tx.getFrom(),
tx.getValue().add(tx.getFee()).add(tx.getGasPrice().multiply(tx.getGas())));

// execute the transaction
SemuxBlock block = kernel.createEmptyBlock();
BlockStore blockStore = new SemuxBlockStore(chain);
TransactionExecutor exec = new TransactionExecutor(config, blockStore);
TransactionResult result = exec.execute(tx, asTrack, dsTrack, block, true, 0);

byte[] contractAddress = type.equals(TransactionType.CREATE)
? HashUtil.calcNewAddress(tx.getFrom(), tx.getNonce())
: null;
return TypeFactory.transactionResultType(result, tx.getFee(), contractAddress, block.getNumber());
}

private static final String IP_ADDRESS_PATTERN = "^([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." +
Expand Down Expand Up @@ -1008,4 +1027,4 @@ public Response createAccountDeprecated(String name, String privateKey) {
public Response deleteAccountDeprecated(String address) {
return deleteAccount(address);
}
}
}
12 changes: 5 additions & 7 deletions src/main/java/org/semux/api/v2/TypeFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
import java.util.stream.Collectors;

import org.ethereum.vm.LogInfo;
import org.ethereum.vm.util.HashUtil;
import org.semux.Kernel;
import org.semux.api.v2.model.AccountType;
import org.semux.api.v2.model.AccountVoteType;
Expand Down Expand Up @@ -162,22 +161,21 @@ public static TransactionType transactionType(Transaction tx) {
return txType;
}

public static TransactionResultType transactionResultType(Transaction tx, TransactionResult result, long number) {
public static TransactionResultType transactionResultType(TransactionResult result, Amount fee,
byte[] contractAddress, long blockNumber) {
return new TransactionResultType()
.blockNumber(Long.toString(number))
.blockNumber(Long.toString(blockNumber))
.code(result.getCode().name())
.logs(result.getLogs().stream().map(TypeFactory::logInfoType).collect(Collectors.toList()))
.gas(Long.toString(result.getGas()))
.gasUsed(String.valueOf(result.getGasUsed()))
.gasPrice(encodeAmount(result.getGasPrice()))
.fee(encodeAmount(tx.getFee()))
.fee(encodeAmount(fee))
.code(result.getCode().name())
.internalTransactions(result.getInternalTransactions().stream()
.map(TypeFactory::internalTransactionType).collect(Collectors.toList()))
.returnData(Hex.encode0x(result.getReturnData()))
.contractAddress(
tx.getType().equals(CREATE) ? Hex.encode0x(HashUtil.calcNewAddress(tx.getFrom(), tx.getNonce()))
: null);
.contractAddress(contractAddress == null ? null : Hex.encode0x(contractAddress));
}

private static LogInfoType logInfoType(LogInfo log) {
Expand Down
Loading

0 comments on commit 6f91d0e

Please sign in to comment.