Commit 256800a5 authored by zeroleak's avatar zeroleak
Browse files

add server.export.activity

parent d8d80216
......@@ -82,12 +82,18 @@ At the beginning of the mix, only mustMix can register up, to *anonymity-set - l
Liquidities are added as soon as *must-mix-min* and *miner-fee-mix* are reached, up to *anonymity-set* inputs for the mix.
### Exports
Each mix success/fail is appended to a CSV file:
Mixs are exported to a CSV file:
```
server.export.mixs.directory
server.export.mixs.filename
```
Activity is exported to a CSV file:
```
server.export.activity.directory
server.export.activity.filename
```
### Testing
```
server.rpc-client.mock-tx-broadcast = false
......
......@@ -4,7 +4,9 @@ import com.samourai.javaserver.config.ServerConfig;
import com.samourai.javaserver.run.ServerApplication;
import com.samourai.javaserver.utils.LogbackUtils;
import com.samourai.javaserver.utils.ServerUtils;
import com.samourai.whirlpool.server.beans.export.ActivityCsv;
import com.samourai.whirlpool.server.config.WhirlpoolServerConfig;
import com.samourai.whirlpool.server.services.ExportService;
import com.samourai.whirlpool.server.services.rpc.RpcClientService;
import com.samourai.whirlpool.server.utils.Utils;
import com.samourai.xmanager.client.XManagerClient;
......@@ -27,6 +29,8 @@ public class Application extends ServerApplication {
@Autowired private RpcClientService rpcClientService;
@Autowired private ExportService exportService;
@Autowired private WhirlpoolServerConfig serverConfig;
@Autowired private XManagerClient xManagerClient;
......@@ -47,6 +51,10 @@ public class Application extends ServerApplication {
xManagerClient.getAddressIndexOrDefault(XManagerService.WHIRLPOOL);
log.info("XM index: " + addressIndexResponse.index);
// log activity
ActivityCsv activityCsv = new ActivityCsv("STARTUP", null, null, null, null, null);
exportService.exportActivity(activityCsv);
// server starting...
}
......
package com.samourai.whirlpool.server.beans;
public enum Activity {
REGISTER_INPUT,
CONFIRM_INPUT,
DISCONNECT
}
package com.samourai.whirlpool.server.beans;
import com.samourai.whirlpool.server.beans.export.ActivityCsv;
import com.samourai.whirlpool.server.beans.rpc.TxOutPoint;
public class RegisteredInput {
......@@ -19,20 +18,6 @@ public class RegisteredInput {
this.lastUserHash = lastUserHash;
}
public ActivityCsv toActivity(Activity activity, String poolId, String headers) {
return new ActivityCsv(
activity,
poolId,
outPoint.getHash() + ":" + outPoint.getIndex(),
outPoint.getValue(),
outPoint.getConfirmations(),
liquidity,
username,
lastUserHash,
ip,
headers);
}
public String getUsername() {
return username;
}
......
package com.samourai.whirlpool.server.beans.export;
import com.opencsv.bean.CsvBindByPosition;
import com.samourai.whirlpool.server.beans.Activity;
import com.samourai.whirlpool.server.beans.RegisteredInput;
import com.samourai.whirlpool.server.beans.rpc.TxOutPoint;
import java.sql.Timestamp;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
public class ActivityCsv {
public static final String[] HEADERS =
new String[] {
"date",
"activity",
"poolId",
"utxo",
"value",
"liquidity",
"client",
"userHash",
"username",
"ip"
};
new String[] {"date", "activity", "poolId", "arg", "details", "ip", "clientDetails"};
@CsvBindByPosition(position = 0)
private Timestamp date;
@CsvBindByPosition(position = 1)
private Activity activity;
private String activity;
@CsvBindByPosition(position = 2)
private String poolId;
@CsvBindByPosition(position = 3)
private String utxo;
private String arg;
@CsvBindByPosition(position = 4)
private long value;
private String details;
@CsvBindByPosition(position = 5)
private int confirmations;
@CsvBindByPosition(position = 6)
private boolean liquidity;
@CsvBindByPosition(position = 7)
private String username;
@CsvBindByPosition(position = 8)
private String userHash;
@CsvBindByPosition(position = 9)
private String ip;
@CsvBindByPosition(position = 10)
private String headers;
@CsvBindByPosition(position = 6)
private String clientDetails;
public ActivityCsv(
Activity activity,
String activity,
String poolId,
String arg,
Map<String, String> details,
String ip,
Map<String, String> clientDetails) {
init(activity, poolId, arg, details, ip, clientDetails);
}
private void init(
String activity,
String poolId,
String utxo,
long value,
int confirmations,
boolean liquidity,
String username,
String userHash,
String arg,
Map<String, String> details,
String ip,
String headers) {
Map<String, String> clientDetails) {
this.date = new Timestamp(System.currentTimeMillis());
this.activity = activity;
this.poolId = poolId;
this.utxo = utxo;
this.value = value;
this.confirmations = confirmations;
this.liquidity = liquidity;
this.username = username;
this.userHash = userHash;
this.arg = arg;
this.details = details != null ? details.toString() : null;
this.ip = ip;
this.headers = headers;
this.clientDetails = clientDetails != null ? clientDetails.toString() : null;
}
public Timestamp getDate() {
return date;
public ActivityCsv(
String activity,
String poolId,
String arg,
Map<String, String> details,
HttpServletRequest request) {
this.clientDetails = computeClientDetails(request).toString();
init(activity, poolId, arg, details, request != null ? request.getRemoteAddr() : null, null);
}
public Activity getActivity() {
return activity;
public ActivityCsv(
String activity,
String poolId,
Map<String, String> details,
String ip,
Map<String, String> clientDetails) {
init(activity, poolId, null, details, ip, clientDetails);
}
public String getPoolId() {
return poolId;
public ActivityCsv(
String activity,
String poolId,
RegisteredInput registeredInput,
Map<String, String> details,
Map<String, String> clientDetails) {
// arg
TxOutPoint outPoint = registeredInput.getOutPoint();
String arg = outPoint.getHash() + ":" + outPoint.getIndex();
// details
if (details == null) {
details = new LinkedHashMap<>();
}
details.put("confs", Integer.toString(outPoint.getConfirmations()));
details.put("value", Long.toString(outPoint.getValue()));
details.put("liquidity", Boolean.toString(registeredInput.isLiquidity()));
// clientDetails
if (clientDetails == null) {
clientDetails = new LinkedHashMap<>();
}
clientDetails.put("u", registeredInput.getUsername());
if (registeredInput.getLastUserHash() != null) {
clientDetails.put("userHash", registeredInput.getLastUserHash());
}
init(activity, poolId, arg, details, registeredInput.getIp(), clientDetails);
}
public String getUtxo() {
return utxo;
private Map<String, String> computeClientDetails(HttpServletRequest request) {
if (request == null) {
return null;
}
Map<String, String> clientDetails = new LinkedHashMap<>();
Enumeration<String> names = request.getHeaderNames();
while (names.hasMoreElements()) {
String name = names.nextElement();
String value = request.getHeader(name);
clientDetails.put(name, value);
}
return clientDetails;
}
public long getValue() {
return value;
public Timestamp getDate() {
return date;
}
public int getConfirmations() {
return confirmations;
public String getActivity() {
return activity;
}
public boolean isLiquidity() {
return liquidity;
public String getArg() {
return arg;
}
public String getUsername() {
return username;
public String getDetails() {
return details;
}
public String getUserHash() {
return userHash;
public String getPoolId() {
return poolId;
}
public String getIp() {
return ip;
}
public String getHeaders() {
return headers;
public String getClientDetails() {
return clientDetails;
}
}
......@@ -608,6 +608,8 @@ public class WhirlpoolServerConfig extends ServerConfig {
+ String.valueOf(revealOutput.timeout);
configInfo.put("timeouts", timeoutInfo);
configInfo.put("export.mixs", export.mixs.directory + " -> " + export.mixs.filename);
configInfo.put(
"export.activity", export.activity.directory + " -> " + export.activity.filename);
configInfo.put(
"ban",
"blames="
......
......@@ -18,7 +18,7 @@ public class IpHandshakeInterceptor implements HandshakeInterceptor {
throws Exception {
// Set ip attribute to WebSocket session
attributes.put(ATTR_IP, request.getRemoteAddress().toString());
attributes.put(ATTR_IP, request.getRemoteAddress().getAddress().getHostAddress());
return true;
}
......
......@@ -3,10 +3,14 @@ package com.samourai.whirlpool.server.controllers.rest;
import com.samourai.whirlpool.protocol.WhirlpoolEndpoint;
import com.samourai.whirlpool.protocol.WhirlpoolProtocol;
import com.samourai.whirlpool.protocol.rest.RegisterOutputRequest;
import com.samourai.whirlpool.server.beans.Mix;
import com.samourai.whirlpool.server.beans.export.ActivityCsv;
import com.samourai.whirlpool.server.services.BlameService;
import com.samourai.whirlpool.server.services.DbService;
import com.samourai.whirlpool.server.services.ExportService;
import com.samourai.whirlpool.server.services.RegisterOutputService;
import java.lang.invoke.MethodHandles;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
......@@ -22,16 +26,21 @@ public class RegisterOutputController extends AbstractRestController {
private RegisterOutputService registerOutputService;
private BlameService blameService;
private DbService dbService;
private ExportService exportService;
@Autowired
public RegisterOutputController(
RegisterOutputService registerOutputService, DbService dbService) {
RegisterOutputService registerOutputService,
DbService dbService,
ExportService exportService) {
this.registerOutputService = registerOutputService;
this.dbService = dbService;
this.exportService = exportService;
}
@RequestMapping(value = WhirlpoolEndpoint.REST_REGISTER_OUTPUT, method = RequestMethod.POST)
public void registerOutput(@RequestBody RegisterOutputRequest payload) throws Exception {
public void registerOutput(HttpServletRequest request, @RequestBody RegisterOutputRequest payload)
throws Exception {
if (log.isDebugEnabled()) {
log.debug("(<) " + WhirlpoolEndpoint.REST_REGISTER_OUTPUT);
}
......@@ -39,7 +48,13 @@ public class RegisterOutputController extends AbstractRestController {
// register output
byte[] unblindedSignedBordereau =
WhirlpoolProtocol.decodeBytes(payload.unblindedSignedBordereau64);
registerOutputService.registerOutput(
payload.inputsHash, unblindedSignedBordereau, payload.receiveAddress);
Mix mix =
registerOutputService.registerOutput(
payload.inputsHash, unblindedSignedBordereau, payload.receiveAddress);
// log activity
String poolId = mix.getPool().getPoolId();
ActivityCsv activityCsv = new ActivityCsv("TX0", poolId, null, null, request);
exportService.exportActivity(activityCsv);
}
}
......@@ -2,18 +2,36 @@ package com.samourai.whirlpool.server.controllers.rest;
import com.samourai.javaserver.rest.AbstractRestExceptionHandler;
import com.samourai.whirlpool.protocol.rest.RestErrorResponse;
import com.samourai.whirlpool.server.beans.export.ActivityCsv;
import com.samourai.whirlpool.server.services.ExportService;
import java.lang.invoke.MethodHandles;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.ControllerAdvice;
@ControllerAdvice
public class RestExceptionHandler extends AbstractRestExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private ExportService exportService;
@Autowired
public RestExceptionHandler(ExportService exportService) {
super();
this.exportService = exportService;
}
@Override
protected Object handleError(com.samourai.javaserver.exceptions.NotifiableException e) {
protected Object handleError(
com.samourai.javaserver.exceptions.NotifiableException e, HttpServletRequest request) {
log.warn("RestException -> " + e.getMessage());
// log activity
ActivityCsv activityCsv =
new ActivityCsv("REST:ERROR", request.getRequestURI(), e.getMessage(), null, request);
exportService.exportActivity(activityCsv);
return new RestErrorResponse(e.getMessage());
}
}
package com.samourai.whirlpool.server.controllers.rest;
import com.google.common.collect.ImmutableMap;
import com.samourai.whirlpool.protocol.WhirlpoolEndpoint;
import com.samourai.whirlpool.protocol.WhirlpoolProtocol;
import com.samourai.whirlpool.protocol.rest.Tx0DataResponse;
import com.samourai.whirlpool.server.beans.PoolFee;
import com.samourai.whirlpool.server.beans.export.ActivityCsv;
import com.samourai.whirlpool.server.config.WhirlpoolServerConfig;
import com.samourai.whirlpool.server.services.ExportService;
import com.samourai.whirlpool.server.services.FeeValidationService;
import com.samourai.whirlpool.server.services.PoolService;
import com.samourai.whirlpool.server.utils.Utils;
......@@ -13,7 +16,9 @@ import com.samourai.xmanager.protocol.XManagerService;
import com.samourai.xmanager.protocol.rest.AddressIndexResponse;
import java.lang.invoke.MethodHandles;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
......@@ -29,6 +34,7 @@ public class Tx0Controller extends AbstractRestController {
private PoolService poolService;
private FeeValidationService feeValidationService;
private ExportService exportService;
private WhirlpoolServerConfig serverConfig;
private XManagerClient xManagerClient;
......@@ -36,16 +42,19 @@ public class Tx0Controller extends AbstractRestController {
public Tx0Controller(
PoolService poolService,
FeeValidationService feeValidationService,
ExportService exportService,
WhirlpoolServerConfig serverConfig,
XManagerClient xManagerClient) {
this.poolService = poolService;
this.feeValidationService = feeValidationService;
this.exportService = exportService;
this.serverConfig = serverConfig;
this.xManagerClient = xManagerClient;
}
@RequestMapping(value = WhirlpoolEndpoint.REST_TX0_DATA, method = RequestMethod.GET)
public Tx0DataResponse tx0Data(
HttpServletRequest request,
@RequestParam(value = "poolId", required = true) String poolId,
@RequestParam(value = "scode", required = false) String scode)
throws Exception {
......@@ -121,6 +130,11 @@ public class Tx0Controller extends AbstractRestController {
+ (feeAddress != null ? feeAddress : ""));
}
// log activity
Map<String, String> details = ImmutableMap.of("scode", (scode != null ? scode : "null"));
ActivityCsv activityCsv = new ActivityCsv("TX0", poolId, null, details, request);
exportService.exportActivity(activityCsv);
Tx0DataResponse tx0DataResponse =
new Tx0DataResponse(
feePaymentCode,
......
......@@ -2,30 +2,47 @@ package com.samourai.whirlpool.server.controllers.web;
import com.samourai.javaserver.web.controllers.AbstractErrorWebController;
import com.samourai.javaserver.web.models.ErrorTemplateModel;
import com.samourai.whirlpool.server.beans.export.ActivityCsv;
import com.samourai.whirlpool.server.config.WhirlpoolServerConfig;
import com.samourai.whirlpool.server.services.ExportService;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.ModelAndView;
@RestController
public class ErrorController extends AbstractErrorWebController {
private static final String ENDPOINT = "/error";
private WhirlpoolServerConfig serverConfig;
private ExportService exportService;
@Autowired
public ErrorController(WhirlpoolServerConfig serverConfig) {
public ErrorController(WhirlpoolServerConfig serverConfig, ExportService exportService) {
this.serverConfig = serverConfig;
this.exportService = exportService;
}
@RequestMapping(value = ENDPOINT)
public ModelAndView errorHtml(WebRequest webRequest, HttpServletResponse response, Model model) {
return super.errorHtml(
webRequest, response, model, new ErrorTemplateModel(serverConfig.getName()));
public String errorHtml(
WebRequest request,
HttpServletRequest httpRequest,
HttpServletResponse response,
Model model) {
ErrorTemplateModel errorTemplateModel = new ErrorTemplateModel(serverConfig.getName());
String result = super.errorHtml(request, response, model, errorTemplateModel);
String errorMsg = errorTemplateModel.errorMessage; // was set by super.errorHtml()
// log activity
ActivityCsv activityCsv =
new ActivityCsv("WEB:ERROR", httpRequest.getRequestURI(), errorMsg, null, httpRequest);
exportService.exportActivity(activityCsv);
return result;
}
@Override
......
package com.samourai.whirlpool.server.controllers.websocket;
import com.google.common.collect.ImmutableMap;
import com.samourai.javaserver.exceptions.NotifiableException;
import com.samourai.whirlpool.protocol.WhirlpoolProtocol;
import com.samourai.whirlpool.server.beans.export.ActivityCsv;
import com.samourai.whirlpool.server.config.websocket.IpHandshakeInterceptor;
import com.samourai.whirlpool.server.exceptions.IllegalInputException;
import com.samourai.whirlpool.server.services.ExportService;
import com.samourai.whirlpool.server.services.RegisterInputService;
import com.samourai.whirlpool.server.services.WebSocketService;
import java.lang.invoke.MethodHandles;
import java.security.Principal;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.ArrayUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
public abstract class AbstractWebSocketController {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private WebSocketService webSocketService;
private ExportService exportService;
public AbstractWebSocketController(WebSocketService webSocketService) {
public AbstractWebSocketController(
WebSocketService webSocketService, ExportService exportService) {
this.webSocketService = webSocketService;
this.exportService = exportService;
}
protected void validateHeaders(StompHeaderAccessor headers) throws Exception {
......@@ -35,20 +48,63 @@ public abstract class AbstractWebSocketController {
return NotifiableException.class.isAssignableFrom(e.getClass());
}