Commit 1cf48d9e authored by zeroleak's avatar zeroleak
Browse files

use java-websocket-server

parent b4c51861
......@@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>io.samourai.code.whirlpool</groupId>
<artifactId>whirlpool-server</artifactId>
<version>0.23.21</version>
<version>0.23.22-SNAPSHOT</version>
<name>whirlpool-server</name>
<properties>
<spring-boot.version>2.1.6.RELEASE</spring-boot.version>
......@@ -32,15 +32,20 @@
</exclusions>
</dependency>
<dependency>
<groupId>io.samourai.code.wallet</groupId>
<artifactId>extlibj</artifactId>
<version>0.0.16-fetchWallet</version>
<groupId>io.samourai.code.whirlpool</groupId>
<artifactId>java-websocket-server</artifactId>
<version>1.0.0-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>io.samourai.code.wallet</groupId>
<artifactId>extlibj</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>${spring-boot.version}</version>
<scope>compile</scope>
<groupId>io.samourai.code.wallet</groupId>
<artifactId>extlibj</artifactId>
<version>0.0.18-fetchWallet</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
......@@ -48,12 +53,6 @@
<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>
......@@ -81,6 +80,7 @@
<artifactId>micrometer-registry-prometheus</artifactId>
<version>1.6.0</version>
</dependency>
<!-- test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
......
......@@ -3,6 +3,7 @@ package com.samourai.whirlpool.server.config;
import com.samourai.http.client.HttpUsage;
import com.samourai.javaserver.config.ServerServicesConfig;
import com.samourai.javaserver.utils.ServerUtils;
import com.samourai.javawsserver.config.JWSSConfig;
import com.samourai.wallet.api.explorer.ExplorerApi;
import com.samourai.wallet.bip47.rpc.java.SecretPointFactoryJava;
import com.samourai.wallet.bip47.rpc.secretPoint.ISecretPointFactory;
......@@ -12,6 +13,7 @@ import com.samourai.wallet.util.CryptoTestUtil;
import com.samourai.wallet.util.FormatsUtilGeneric;
import com.samourai.wallet.util.MessageSignUtilGeneric;
import com.samourai.wallet.util.TxUtil;
import com.samourai.whirlpool.protocol.WhirlpoolEndpoint;
import com.samourai.whirlpool.protocol.WhirlpoolProtocol;
import com.samourai.whirlpool.protocol.fee.WhirlpoolFee;
import com.samourai.whirlpool.server.services.JavaHttpClientService;
......@@ -105,4 +107,24 @@ public class ServicesConfig extends ServerServicesConfig {
return new XManagerClient(
serverConfig.isTestnet(), false, httpClient.getHttpClient(HttpUsage.COORDINATOR_REST));
}
@Bean
JWSSConfig jwssConfig() {
String[] endpoints =
new String[] {
WhirlpoolEndpoint.WS_CONNECT,
WhirlpoolEndpoint.WS_REGISTER_INPUT,
WhirlpoolEndpoint.WS_CONFIRM_INPUT,
WhirlpoolEndpoint.WS_REVEAL_OUTPUT,
WhirlpoolEndpoint.WS_SIGNING
};
String WS_PREFIX = "/ws/";
String WS_PREFIX_DESTINATION = "/topic/";
return new JWSSConfig(
endpoints,
WhirlpoolProtocol.WS_PREFIX_USER_PRIVATE,
WS_PREFIX,
WS_PREFIX_DESTINATION, // NOT USED
WhirlpoolProtocol.WS_PREFIX_USER_REPLY);
}
}
package com.samourai.whirlpool.server.config.security;
import com.samourai.javawsserver.config.JWSSConfig;
import com.samourai.javawsserver.config.JWSSWebSocketConfigurationSupport;
import com.samourai.whirlpool.server.services.WSSessionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
@Configuration
public class WSConfigurationSupport extends JWSSWebSocketConfigurationSupport {
@Autowired
public WSConfigurationSupport(JWSSConfig config, WSSessionService sessionService) {
super(config, sessionService);
}
}
package com.samourai.whirlpool.server.config.security;
import com.samourai.javawsserver.config.JWSSConfig;
import com.samourai.javawsserver.config.JWSSWebSocketSecurityConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
@Configuration
public class WSSecurityConfig extends JWSSWebSocketSecurityConfig {
@Autowired
public WSSecurityConfig(JWSSConfig config) {
super(config);
}
}
package com.samourai.whirlpool.server.config.security;
import com.samourai.javaserver.config.ServerServicesConfig;
import com.samourai.javawsserver.config.JWSSConfig;
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.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
......@@ -43,6 +44,13 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
SystemController.ENDPOINT_HEALTH
};
private JWSSConfig config;
@Autowired
public WebSecurityConfig(JWSSConfig config) {
this.config = config;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
String WS_CONNECT_XHR = WhirlpoolEndpoint.WS_CONNECT + "/**";
......@@ -68,7 +76,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
.permitAll()
// public mixing websocket
.antMatchers(ArrayUtils.addAll(WebSocketConfig.WEBSOCKET_ENDPOINTS, WS_CONNECT_XHR))
.antMatchers(ArrayUtils.addAll(config.getWebsocketEndpoints(), WS_CONNECT_XHR))
.permitAll()
.antMatchers(REST_MIX_ENDPOINTS)
.permitAll()
......
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().getAddress().getHostAddress());
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);
}
}
......@@ -2,13 +2,13 @@ package com.samourai.whirlpool.server.controllers.websocket;
import com.google.common.collect.ImmutableMap;
import com.samourai.javaserver.exceptions.NotifiableException;
import com.samourai.javawsserver.interceptors.JWSSIpHandshakeInterceptor;
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 com.samourai.whirlpool.server.services.WSMessageService;
import java.lang.invoke.MethodHandles;
import java.security.Principal;
import java.util.LinkedHashMap;
......@@ -23,12 +23,12 @@ 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 WSMessageService WSMessageService;
private ExportService exportService;
public AbstractWebSocketController(
WebSocketService webSocketService, ExportService exportService) {
this.webSocketService = webSocketService;
WSMessageService WSMessageService, ExportService exportService) {
this.WSMessageService = WSMessageService;
this.exportService = exportService;
}
......@@ -57,7 +57,7 @@ public abstract class AbstractWebSocketController {
NotifiableException notifiable = NotifiableException.computeNotifiableException(e);
String message = notifiable.getMessage();
String username = principal.getName();
webSocketService.sendPrivateError(username, message);
WSMessageService.sendPrivateError(username, message);
// skip healthCheck
if (!RegisterInputService.HEALTH_CHECK_SUCCESS.equals(message)) {
......@@ -71,7 +71,7 @@ public abstract class AbstractWebSocketController {
Map<String, String> clientDetails = computeClientDetails(messageHeaderAccessor);
clientDetails.put("u", username);
Map<String, String> details = ImmutableMap.of("error", message);
String ip = IpHandshakeInterceptor.getIp(messageHeaderAccessor);
String ip = JWSSIpHandshakeInterceptor.getIp(messageHeaderAccessor);
ActivityCsv activityCsv = new ActivityCsv(activity, null, details, ip, clientDetails);
getExportService().exportActivity(activityCsv);
}
......@@ -101,8 +101,8 @@ public abstract class AbstractWebSocketController {
return clientDetails;
}
protected WebSocketService getWebSocketService() {
return webSocketService;
protected WSMessageService getWSMessageService() {
return WSMessageService;
}
protected ExportService getExportService() {
......
......@@ -5,7 +5,7 @@ import com.samourai.whirlpool.protocol.WhirlpoolProtocol;
import com.samourai.whirlpool.protocol.websocket.messages.ConfirmInputRequest;
import com.samourai.whirlpool.server.services.ConfirmInputService;
import com.samourai.whirlpool.server.services.ExportService;
import com.samourai.whirlpool.server.services.WebSocketService;
import com.samourai.whirlpool.server.services.WSMessageService;
import java.lang.invoke.MethodHandles;
import java.security.Principal;
import org.slf4j.Logger;
......@@ -26,10 +26,10 @@ public class ConfirmInputController extends AbstractWebSocketController {
@Autowired
public ConfirmInputController(
WebSocketService webSocketService,
WSMessageService WSMessageService,
ExportService exportService,
ConfirmInputService confirmInputService) {
super(webSocketService, exportService);
super(WSMessageService, exportService);
this.confirmInputService = confirmInputService;
}
......
package com.samourai.whirlpool.server.controllers.websocket;
import com.samourai.javawsserver.interceptors.JWSSIpHandshakeInterceptor;
import com.samourai.whirlpool.protocol.WhirlpoolEndpoint;
import com.samourai.whirlpool.protocol.websocket.messages.RegisterInputRequest;
import com.samourai.whirlpool.server.beans.RegisteredInput;
import com.samourai.whirlpool.server.beans.export.ActivityCsv;
import com.samourai.whirlpool.server.config.websocket.IpHandshakeInterceptor;
import com.samourai.whirlpool.server.exceptions.AlreadyRegisteredInputException;
import com.samourai.whirlpool.server.services.ExportService;
import com.samourai.whirlpool.server.services.RegisterInputService;
import com.samourai.whirlpool.server.services.WebSocketService;
import com.samourai.whirlpool.server.services.WSMessageService;
import java.lang.invoke.MethodHandles;
import java.security.Principal;
import java.util.Map;
......@@ -30,10 +30,10 @@ public class RegisterInputController extends AbstractWebSocketController {
@Autowired
public RegisterInputController(
WebSocketService webSocketService,
WSMessageService WSMessageService,
ExportService exportService,
RegisterInputService registerInputService) {
super(webSocketService, exportService);
super(WSMessageService, exportService);
this.registerInputService = registerInputService;
}
......@@ -47,7 +47,7 @@ public class RegisterInputController extends AbstractWebSocketController {
validateHeaders(headers);
String username = principal.getName();
String ip = IpHandshakeInterceptor.getIp(messageHeaderAccessor);
String ip = JWSSIpHandshakeInterceptor.getIp(messageHeaderAccessor);
if (log.isDebugEnabled()) {
log.debug(
"(<) ["
......
......@@ -4,7 +4,7 @@ import com.samourai.whirlpool.protocol.WhirlpoolEndpoint;
import com.samourai.whirlpool.protocol.websocket.messages.RevealOutputRequest;
import com.samourai.whirlpool.server.services.ExportService;
import com.samourai.whirlpool.server.services.MixService;
import com.samourai.whirlpool.server.services.WebSocketService;
import com.samourai.whirlpool.server.services.WSMessageService;
import java.lang.invoke.MethodHandles;
import java.security.Principal;
import org.slf4j.Logger;
......@@ -25,8 +25,8 @@ public class RevealOutputController extends AbstractWebSocketController {
@Autowired
public RevealOutputController(
WebSocketService webSocketService, ExportService exportService, MixService mixService) {
super(webSocketService, exportService);
WSMessageService WSMessageService, ExportService exportService, MixService mixService) {
super(WSMessageService, exportService);
this.mixService = mixService;
}
......
......@@ -4,7 +4,7 @@ import com.samourai.whirlpool.protocol.WhirlpoolEndpoint;
import com.samourai.whirlpool.protocol.websocket.messages.SigningRequest;
import com.samourai.whirlpool.server.services.ExportService;
import com.samourai.whirlpool.server.services.SigningService;
import com.samourai.whirlpool.server.services.WebSocketService;
import com.samourai.whirlpool.server.services.WSMessageService;
import java.lang.invoke.MethodHandles;
import java.security.Principal;
import org.slf4j.Logger;
......@@ -25,10 +25,10 @@ public class SigningController extends AbstractWebSocketController {
@Autowired
public SigningController(
WebSocketService webSocketService,
WSMessageService WSMessageService,