Commit 9e7501ec authored by zeroleak's avatar zeroleak
Browse files

batch: fix mix history

parent 4d53de61
......@@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.github.Samourai-Wallet</groupId>
<artifactId>whirlpool-server</artifactId>
<version>develop-SNAPSHOT</version>
<version>develop-fixhistory</version>
<name>whirlpool-server</name>
<properties>
<spring-boot.version>2.1.6.RELEASE</spring-boot.version>
......@@ -29,6 +29,10 @@
<groupId>com.github.Samourai-Wallet</groupId>
<artifactId>extlibj</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
......@@ -38,16 +42,10 @@
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<artifactId>spring-boot-starter</artifactId>
<version>${spring-boot.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-messaging</artifactId>
<version>${spring-security.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
......
......@@ -5,6 +5,7 @@ import com.samourai.javaserver.run.ServerApplication;
import com.samourai.javaserver.utils.LogbackUtils;
import com.samourai.javaserver.utils.ServerUtils;
import com.samourai.whirlpool.server.config.WhirlpoolServerConfig;
import com.samourai.whirlpool.server.services.DbService;
import com.samourai.whirlpool.server.services.rpc.RpcClientService;
import com.samourai.whirlpool.server.utils.Utils;
import com.samourai.xmanager.client.XManagerClient;
......@@ -30,6 +31,7 @@ public class Application extends ServerApplication {
@Autowired private WhirlpoolServerConfig serverConfig;
@Autowired private XManagerClient xManagerClient;
@Autowired private DbService dbService;
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
......@@ -48,6 +50,9 @@ public class Application extends ServerApplication {
log.info("XM index: " + addressIndexResponse.index);
// server starting...
dbService.fixMixHistory();
exit();
}
@Override
......
package com.samourai.whirlpool.server.config.security;
import com.samourai.javaserver.config.ServerServicesConfig;
import com.samourai.whirlpool.protocol.WhirlpoolEndpoint;
import com.samourai.whirlpool.server.config.websocket.WebSocketConfig;
import com.samourai.whirlpool.server.controllers.rest.SystemController;
import com.samourai.whirlpool.server.controllers.web.*;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private static final String[] REST_MIX_ENDPOINTS =
new String[] {
WhirlpoolEndpoint.REST_POOLS,
WhirlpoolEndpoint.REST_REGISTER_OUTPUT,
WhirlpoolEndpoint.REST_TX0_DATA,
SystemController.ENDPOINT_HEALTH
};
@Override
protected void configure(HttpSecurity http) throws Exception {
String WS_CONNECT_XHR = WhirlpoolEndpoint.WS_CONNECT + "/**";
// disable csrf for our endpoints
http.csrf()
.ignoringAntMatchers(ArrayUtils.addAll(REST_MIX_ENDPOINTS, WS_CONNECT_XHR))
.and()
.authorizeRequests()
// public statics
.antMatchers(ServerServicesConfig.STATICS)
.permitAll()
// public login form
.antMatchers(LoginWebController.ENDPOINT)
.permitAll()
.antMatchers(LoginWebController.PROCESS_ENDPOINT)
.permitAll()
// public mixing websocket
.antMatchers(ArrayUtils.addAll(WebSocketConfig.WEBSOCKET_ENDPOINTS, WS_CONNECT_XHR))
.permitAll()
.antMatchers(REST_MIX_ENDPOINTS)
.permitAll()
// restrict admin
.antMatchers(StatusWebController.ENDPOINT)
.hasAnyAuthority(WhirlpoolPrivilege.STATUS.toString(), WhirlpoolPrivilege.ALL.toString())
.antMatchers(HistoryWebController.ENDPOINT)
.hasAnyAuthority(WhirlpoolPrivilege.HISTORY.toString(), WhirlpoolPrivilege.ALL.toString())
.antMatchers(ConfigWebController.ENDPOINT)
.hasAnyAuthority(WhirlpoolPrivilege.CONFIG.toString(), WhirlpoolPrivilege.ALL.toString())
.antMatchers(BanWebController.ENDPOINT)
.hasAnyAuthority(WhirlpoolPrivilege.BAN.toString(), WhirlpoolPrivilege.ALL.toString())
.antMatchers(SystemWebController.ENDPOINT)
.hasAnyAuthority(WhirlpoolPrivilege.SYSTEM.toString(), WhirlpoolPrivilege.ALL.toString())
// reject others
.anyRequest()
.denyAll()
.and()
// custom login form
.formLogin()
.loginProcessingUrl(LoginWebController.PROCESS_ENDPOINT)
.loginPage(LoginWebController.ENDPOINT)
.defaultSuccessUrl(StatusWebController.ENDPOINT, true);
}
@Bean
public DaoAuthenticationProvider authenticationProvider(
WhirlpoolUserDetailsService whirlpoolUserDetailsService) {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(whirlpoolUserDetailsService);
authProvider.setPasswordEncoder(encoder());
return authProvider;
}
@Bean
public PasswordEncoder encoder() {
return new BCryptPasswordEncoder(11);
}
}
package com.samourai.whirlpool.server.config.security;
import com.samourai.whirlpool.server.config.websocket.WebSocketConfig;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.messaging.MessageSecurityMetadataSourceRegistry;
import org.springframework.security.config.annotation.web.socket.AbstractSecurityWebSocketMessageBrokerConfigurer;
@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
@Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
super.configureInbound(messages);
messages
// allow websocket server endpoints
.simpMessageDestMatchers(WebSocketConfig.WEBSOCKET_ENDPOINTS)
.permitAll()
// deny any other messages (including client-to-client)
.simpMessageDestMatchers("/**")
.denyAll();
}
@Override
protected boolean sameOriginDisabled() {
return true;
}
}
package com.samourai.whirlpool.server.config.websocket;
import com.sun.security.auth.UserPrincipal;
import java.lang.invoke.MethodHandles;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageHeaderAccessor;
/**
* Assign a principal for each websocket client. This is needed to be able to communicate with a
* specific client.
*/
public class AssignPrincipalChannelInterceptor implements ChannelInterceptor {
private static Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor =
MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
if (log.isTraceEnabled()) {
log.trace(
"Assigning principal from sessionId: login="
+ (accessor.getLogin() != null ? accessor.getLogin() : "null")
+ ",sessionId="
+ accessor.getSessionId());
}
accessor.setUser(new UserPrincipal(accessor.getSessionId()));
}
return message;
}
}
package com.samourai.whirlpool.server.config.websocket;
import java.util.Map;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
public class IpHandshakeInterceptor implements HandshakeInterceptor {
private static final String ATTR_IP = "ip";
public boolean beforeHandshake(
ServerHttpRequest request,
ServerHttpResponse response,
WebSocketHandler wsHandler,
Map<String, Object> attributes)
throws Exception {
// Set ip attribute to WebSocket session
attributes.put(ATTR_IP, request.getRemoteAddress().toString());
return true;
}
public void afterHandshake(
ServerHttpRequest request,
ServerHttpResponse response,
WebSocketHandler wsHandler,
Exception exception) {}
public static String getIp(SimpMessageHeaderAccessor messageHeaderAccessor) {
return (String)
messageHeaderAccessor.getSessionAttributes().get(IpHandshakeInterceptor.ATTR_IP);
}
}
package com.samourai.whirlpool.server.config.websocket;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.samourai.whirlpool.protocol.WhirlpoolEndpoint;
import com.samourai.whirlpool.protocol.WhirlpoolProtocol;
import com.samourai.whirlpool.server.services.WebSocketSessionService;
import java.lang.invoke.MethodHandles;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
import org.springframework.messaging.converter.DefaultContentTypeResolver;
import org.springframework.messaging.converter.MappingJackson2MessageConverter;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
import org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandler;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.util.MimeTypeUtils;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurationSupport;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration;
import org.springframework.web.socket.messaging.SessionConnectEvent;
import org.springframework.web.socket.messaging.SessionDisconnectEvent;
import org.springframework.web.socket.messaging.SessionSubscribeEvent;
/** Websocket configuration with STOMP. */
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport
implements WebSocketMessageBrokerConfigurer {
private static Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static final int HEARTBEAT_DELAY = 20000;
public static String[] WEBSOCKET_ENDPOINTS =
new String[] {
WhirlpoolEndpoint.WS_CONNECT,
WhirlpoolEndpoint.WS_REGISTER_INPUT,
WhirlpoolEndpoint.WS_CONFIRM_INPUT,
WhirlpoolEndpoint.WS_REVEAL_OUTPUT,
WhirlpoolEndpoint.WS_SIGNING
};
@Autowired private ObjectMapper objectMapper;
@Autowired private WhirlpoolProtocol whirlpoolProtocol;
@Autowired private WebSocketSessionService webSocketSessionService;
@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registry) {
super.configureWebSocketTransport(registry);
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
super.configureClientInboundChannel(registration);
registration.interceptors(new AssignPrincipalChannelInterceptor());
}
@Override
public void configureClientOutboundChannel(ChannelRegistration registration) {
super.configureClientOutboundChannel(registration);
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
super.addArgumentResolvers(argumentResolvers);
}
@Override
public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> returnValueHandlers) {
super.addReturnValueHandlers(returnValueHandlers);
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry
.addEndpoint(WEBSOCKET_ENDPOINTS)
.setAllowedOrigins("*")
.addInterceptors(new IpHandshakeInterceptor())
.withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
//// registry.setPreservePublishOrder()
// enable heartbeat (mandatory to detect client disconnect)
ThreadPoolTaskScheduler te = new ThreadPoolTaskScheduler();
te.setPoolSize(1);
te.setThreadNamePrefix("wss-heartbeat-thread-");
te.initialize();
registry
////// .setApplicationDestinationPrefixes(WS_WEBSOCKET_ENDPOINTS.WS_PREFIX)
.enableSimpleBroker(whirlpoolProtocol.WS_PREFIX_USER_REPLY)
.setHeartbeatValue(new long[] {HEARTBEAT_DELAY, HEARTBEAT_DELAY})
.setTaskScheduler(te);
registry.setUserDestinationPrefix(whirlpoolProtocol.WS_PREFIX_USER_PRIVATE);
}
@Override
public boolean configureMessageConverters(List<MessageConverter> messageConverters) {
DefaultContentTypeResolver resolver = new DefaultContentTypeResolver();
resolver.setDefaultMimeType(MimeTypeUtils.APPLICATION_JSON);
MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
converter.setObjectMapper(objectMapper);
converter.setContentTypeResolver(resolver);
messageConverters.add(converter);
return false;
}
// listeners for logging purpose
@EventListener
public void handleSubscribeEvent(SessionSubscribeEvent event) {
String username = event.getUser() != null ? event.getUser().getName() : "unknown";
if (log.isDebugEnabled()) {
log.debug("(<) " + username + " subscribe");
}
}
@EventListener
public void handleConnectEvent(SessionConnectEvent event) {
String username = event.getUser() != null ? event.getUser().getName() : "unknown";
if (log.isDebugEnabled()) {
log.debug("(<) " + username + " connect");
}
webSocketSessionService.onConnect(username);
}
@EventListener
public void handleDisconnectEvent(SessionDisconnectEvent event) {
String username = event.getUser() != null ? event.getUser().getName() : "unknown";
if (log.isDebugEnabled()) {
log.debug("(<) " + username + " disconnect");
}
webSocketSessionService.onDisconnect(username);
}
}
package com.samourai.whirlpool.server.controllers.rest;
public abstract class AbstractRestController {
public AbstractRestController() {}
}
package com.samourai.whirlpool.server.controllers.rest;
import com.samourai.whirlpool.protocol.WhirlpoolEndpoint;
import com.samourai.whirlpool.protocol.rest.PoolInfo;
import com.samourai.whirlpool.protocol.rest.PoolsResponse;
import com.samourai.whirlpool.server.beans.Mix;
import com.samourai.whirlpool.server.beans.Pool;
import com.samourai.whirlpool.server.config.WhirlpoolServerConfig;
import com.samourai.whirlpool.server.services.FeeValidationService;
import com.samourai.whirlpool.server.services.PoolService;
import java.lang.invoke.MethodHandles;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class PoolsController extends AbstractRestController {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private PoolService poolService;
private FeeValidationService feeValidationService;
private WhirlpoolServerConfig serverConfig;
@Autowired
public PoolsController(
PoolService poolService,
FeeValidationService feeValidationService,
WhirlpoolServerConfig serverConfig) {
this.poolService = poolService;
this.feeValidationService = feeValidationService;
this.serverConfig = serverConfig;
}
@RequestMapping(value = WhirlpoolEndpoint.REST_POOLS, method = RequestMethod.GET)
public PoolsResponse pools() {
PoolInfo[] pools =
poolService
.getPools()
.parallelStream()
.map(pool -> computePoolInfo(pool))
.toArray((i) -> new PoolInfo[i]);
PoolsResponse poolsResponse = new PoolsResponse(pools);
return poolsResponse;
}
private PoolInfo computePoolInfo(Pool pool) {
Mix currentMix = pool.getCurrentMix();
int nbRegistered =
currentMix.getNbConfirmingInputs()
+ pool.getMustMixQueue().getSize()
+ pool.getLiquidityQueue().getSize();
int nbConfirmed = currentMix.getNbInputs();
PoolInfo poolInfo =
new PoolInfo(
pool.getPoolId(),
pool.getDenomination(),
pool.getPoolFee().getFeeValue(),
pool.computeMustMixBalanceMin(),
pool.computeMustMixBalanceCap(),
pool.computeMustMixBalanceMax(),
pool.getAnonymitySet(),
pool.getMinMustMix(),
nbRegistered,
pool.getAnonymitySet(),
currentMix.getMixStatus(),
currentMix.getElapsedTime(),
nbConfirmed);
return poolInfo;
}
}
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.services.BlameService;
import com.samourai.whirlpool.server.services.DbService;
import com.samourai.whirlpool.server.services.RegisterOutputService;
import java.lang.invoke.MethodHandles;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class RegisterOutputController extends AbstractRestController {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private RegisterOutputService registerOutputService;
private BlameService blameService;
private DbService dbService;
@Autowired
public RegisterOutputController(
RegisterOutputService registerOutputService, DbService dbService) {
this.registerOutputService = registerOutputService;
this.dbService = dbService;
}
@RequestMapping(value = WhirlpoolEndpoint.REST_REGISTER_OUTPUT, method = RequestMethod.POST)
public void registerOutput(@RequestBody RegisterOutputRequest payload) throws Exception {
if (log.isDebugEnabled()) {
log.debug("(<) " + WhirlpoolEndpoint.REST_REGISTER_OUTPUT);
}
// register output
byte[] unblindedSignedBordereau =
WhirlpoolProtocol.decodeBytes(payload.unblindedSignedBordereau64);
registerOutputService.registerOutput(
payload.inputsHash, unblindedSignedBordereau, payload.receiveAddress);
}
}
package com.samourai.whirlpool.server.controllers.rest;
import com.samourai.javaserver.rest.AbstractRestExceptionHandler;
import com.samourai.whirlpool.protocol.rest.RestErrorResponse;
import java.lang.invoke.MethodHandles;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
@ControllerAdvice
public class RestExceptionHandler extends AbstractRestExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
@Override
protected Object handleError(com.samourai.javaserver.exceptions.NotifiableException e) {
log.warn("RestException -> " + e.getMessage());
return new RestErrorResponse(e.getMessage());
}
}
package com.samourai.whirlpool.server.controllers.rest;
import com.samourai.whirlpool.protocol.WhirlpoolEndpoint;
import com.samourai.whirlpool.server.controllers.rest.beans.HealthResponse;
import com.samourai.whirlpool.server.services.HealthService;
import java.lang.invoke.MethodHandles;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;