Unable to Send Binary Data (byte[]) using SimpMessagingTemplate - java

I am able to send string messages to websocket using SimpMessagingTemplate.convertAndSend() method, but same is not working when I try to send byte[]. When I send byte[] to the websocket subscription channel, a websocket DISCONNECT event is getting triggered and connection is getting lost. Any Idea to send byte[] to websocket using SimpMessagingTemplate !!!!!
#Autowired
private SimpMessagingTemplate template;
String body = "Message to be Sent";
template.convertAndSend("/channel", body); --------- working
template.convertAndSend("/channel", body.getBytes()); --------- Not working

Have you tried adding a custom message converter to the byte array?. Just override configureMessageConverters method.
#Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
#Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages.simpTypeMatchers(
SimpMessageType.DISCONNECT, SimpMessageType.OTHER).permitAll();
messages.anyMessage().authenticated();
}
#Override
public boolean configureMessageConverters(List<MessageConverter> messageConverters) {
messageConverters.add(new ByteArrayMessageConverter());
return false;
}
#Override
protected boolean sameOriginDisabled() {
return true;
}
}

Related

Spring boot Restcontroller and WebSocket without STOMP

I have the following problem.
I'm trying to develop a Spring Boot application that serves as RestController and also uses Websockets (w/o STOMP). The RestController has an universal GetMapping method ("/{name}/**) that fetches, depending on name variable, content out of a template database.
My websocket handler should react as broadcast message broker for calls at the endpoint ("/broadcast").
When I test it with Postman, the broadcast websocket call just calls my Restcontroller, what is not intended. It should connect to the Websocket handler.
My code looks like this:
WebSocketConfig:
#Configuration
#EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
#Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(webSocketBroadcastHandler(), "/broadcast").setAllowedOrigins("*");
}
#Bean
public WebSocketHandler webSocketBroadcastHandler() {
CfWebSocketBroadcastHandler swsh = new CfWebSocketBroadcastHandler();
return swsh;
}
}
Broadcast handler:
public class CfWebSocketBroadcastHandler extends TextWebSocketHandler {
private static Set<WebSocketSession> sessions = null;
public CfWebSocketBroadcastHandler() {
sessions = new CopyOnWriteArraySet<>();
}
#Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
sessions.add(session);
}
#Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
sessions.remove(session);
}
#Override
public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
if (!sessions.contains(session)) {
sessions.add(session);
}
String request = message.getPayload();
WebSocketBroadcastMessage bcm = new Gson().fromJson(request, WebSocketBroadcastMessage.class);
broadcast(bcm);
}
public void broadcast(WebSocketBroadcastMessage bcm) {
for (WebSocketSession session : sessions) {
if (session.isOpen()) {
try {
session.sendMessage(new TextMessage(new Gson().toJson(bcm)));
} catch (IOException ex) {
Logger.getLogger(CfWebSocketBroadcastHandler.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
}
}
RestController:
#RestController
#Component
#Configuration
public class MyRestcontroller {
#GetMapping(path = "/{name}/**")
public void universalGet(#PathVariable("name") String name, #Context HttpServletRequest request, #Context HttpServletResponse response) {
// get template from databse with name variable
}
}
How can I make sure, that the websocket handler gets called instead of the restcontroller?
Further infos:
I'm using spring-boot 2.6.7 and embedded Tomcat 9.0.0.M6.
The maven dependencies are included.
Thanks for any help.

Why can I subscribe to destination but I don't receive any messages for that user with Spring WebSocket STOMP (with Spring Security and Keycloak)?

I'm trying to implement a microservice for sending messages via WebSocket.
I can send correctly messages to a client subcribed and authenticated (passing the JWT token to the WebClient server), but now I want send messages only to a particular user.
Following the official spring documentation I can have the client correctly subscribed but I don't get any message.
WebSocket Server configuration:
WebSecurityConfig.java
[...]
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.cors()
.and().csrf().disable()
.authorizeRequests()
.anyRequest()
.authenticated()
;
}
#Override
public void configure(WebSecurity web) {
web.ignoring()
.antMatchers("/ws/**");
}
[...]
WebSocketConfig.java
#Slf4j
#Configuration
//#EnableScheduling
#EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
#Qualifier("websocket")
private AuthenticationManager authenticationManager;
#Autowired
public WebSocketConfig(AuthenticationManager authenticationManager)
{
this.authenticationManager = authenticationManager;
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").setAllowedOrigins("*")
registry.addEndpoint("/ws").setAllowedOrigins("*").withSockJS();
}
#Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptor() {
#Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor headerAccessor =
MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(headerAccessor.getCommand()))
{
// log.info("TOKEN: {}", headerAccessor.getNativeHeader("token").toString());
Optional.ofNullable(headerAccessor.getNativeHeader("token")).ifPresent(ah ->
{
String bearerToken = ah.get(0).replace("Bearer ", "");
JWSAuthenticationToken token = (JWSAuthenticationToken) authenticationManager
.authenticate(new JWSAuthenticationToken(bearerToken));
headerAccessor.setUser(token);
});
}
return message;
}
});
}
#Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/app");
registry.enableSimpleBroker("/topic", "/queue");
}
}
WebSocketSecurityConfig.java
#Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
#Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages
.simpTypeMatchers(CONNECT, HEARTBEAT, UNSUBSCRIBE, DISCONNECT).permitAll()
.simpDestMatchers("/app/**", "/topic/**", "/queue/**").authenticated()
.simpSubscribeDestMatchers("/topic/**", "/queue/**", "/user/**").authenticated()
.anyMessage().denyAll();
}
#Override
protected boolean sameOriginDisabled() {
//disable CSRF for websockets for now...
return true;
}
}
In a Java class I tryied both convertAndSend and convertAndSendToUser (probably a better choice) functions for send the message:
simpMessagingTemplate.convertAndSend("/user/" + username + "/queue/private-messages", message);
simpMessagingTemplate.convertAndSendToUser("user","/queue/private-messages", message);
WebSocket Client configurations
WebSecurityConfig.java
[...]
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.cors()
.and().csrf().disable()
.authorizeRequests()
.anyRequest()
.authenticated()
;
}
[...]
StompClient.java where i use the function createAndConnectClient for create the connection
[...]
public void createAndConnectClient(String accessToken) {
WebSocketClient client = new StandardWebSocketClient();
WebSocketStompClient stompClient = new WebSocketStompClient(client);
stompClient.setMessageConverter(new MappingJackson2MessageConverter());
StompSessionHandler sessionHandler = new MyStompSessionHandler(stompConfig);
// connect with custom headers
final WebSocketHttpHeaders headers = new WebSocketHttpHeaders();
final StompHeaders head = new StompHeaders();
head.add("token", accessToken);
stompClient.connect(serverURL, headers, head, sessionHandler);
}
[...]
MyStompSessionHandler
#Component
public class MyStompSessionHandler extends StompSessionHandlerAdapter {
private final StompConfig stompConfig;
#Autowired
public MyStompSessionHandler(StompConfig stompConfig) {
this.stompConfig = stompConfig;
}
#Override
public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
log.info("New session established : " + session.getSessionId());
session.subscribe("/user/queue/private-messages", this);
log.info("Subscribed to /user/queue/private-messages");
}
#Override
public void handleException(StompSession session, StompCommand command, StompHeaders headers, byte[] payload, Throwable exception) {
log.error("Got an exception", exception);
}
#Override
public Type getPayloadType(StompHeaders headers) {
return Message.class;
}
#Override
public void handleFrame(StompHeaders headers, Object payload) {
if (payload != null) {
Message msg = (Message) payload;
log.info("Received : " + msg.getText() + " from : " + msg.getFrom());
} else {
log.info("NULL Payload");
}
}
}
As I said I can subscribe to the destination but I don't receive any message.
EDIT: I added this code when I send the message
[...]
SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
JWSAuthenticationToken token = null;
try {
token = userService.getToken(message.getTo());
log.info("TOKEN BEFORE SEND: {}", token);
} catch (NotFoundException e) {
e.printStackTrace();
log.error("User isn't connected");
}
headerAccessor.setUser(token);
headerAccessor.setSessionId(userService.getSessionId(message.getTo()));
log.info("SESSION-ID: {}", headerAccessor.getSessionId());
log.info("HEADER: {}", headerAccessor.toString());
simpMessagingTemplate.convertAndSendToUser(message.getTo(), webSocketConfig.getDestination(), message, headerAccessor.getMessageHeaders());
[...]
and so... I'm sure the user (and the SessionID too) is the same. but still not receinving nothing on my client!
WORKING SOLUTION:
Not the thing I want but... it works! ¯_(ツ)_/¯
(no benefit from STOMP mechanisms for users management)
When I subscribe to destination, in my Client, I add to destionation url the name of the user
"/queue/messages-" + user.getName().
Using convertAndSend on that destination (Server side that send messages and without headers) I can send private messages to user.
I believe this is happening because the DefaultSimpUserRegistry is not getting updated with the user. If you see this line where it is supposed to add the user, it does not add from the STOMP headerAccesssor but from the from the sub-protocol i.e websocket protocol. So you will have to set the user for the websocket session.
private void populateSecurityContextHolder(String
username, List<SimpleGrantedAuthority> grantedAuthorities) {
PreAuthenticatedAuthenticationToken authToken = new PreAuthenticatedAuthenticationToken(
username, null, grantedAuthorities);
SecurityContextHolder.getContext().setAuthentication(authToken);
}
So on setting this, the user registry should get updated with the user and hence when the message is sent to the queue when it finds the user it should be able to send it to the user, rather than discarding the message because the user was not present.
Did you set the UserDestinationPrefix for SimpMessagingTemplate. If not can you please try setting that to '/user & see if it works.
Not the thing I want but... it works! ¯_(ツ)_/¯
(no benefit from STOMP mechanisms for users management)
When I subscribe to destination, in my Client, I add to destionation url the name of the user:
"/queue/messages-" + user.getName().
Using convertAndSend on that destination (Server side that send messages) I can send private messages to user.

How to check if websocket user is authenticated when subscribe a channel?

I have websocket config class with :
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topics");
config.setApplicationDestinationPrefixes("/app");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/notifications").setAllowedOrigins("*").withSockJS();
}
As well as ClientInboundChannel:
#Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.setInterceptors(new ChannelInterceptorAdapter() {
#Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
final String xAuthToken = accessor.getFirstNativeHeader(ManipulatesHeaders.X_AUTH_TOKEN_HEADER);
if (xAuthToken == null) {
return message;
}
final UserDetails userDetails = authService.getUserDetails(xAuthToken);
if (StompCommand.CONNECT == accessor.getCommand()) {
final WebSocketPrincipal principal = (...)
userRegistry.registerUser(principal, message);
accessor.setUser(principal);
}
return message;
}
}
}
Now, I would like to send a welcome messsage to every user that subscribe a particular channel. It's obvious that It might be achieved by creating a class that implements ApplicationListener<SessionSubscribeEvent> and provides
#Override
public void onApplicationEvent(final SessionSubscribeEventevent) {
StompHeaderAccessor sha = StompHeaderAccessor.wrap(event.getMessage());
}
Second approach is to perform similar thing in aforementioned ClientInboundChannel (it's the same).
The problem is, that there is no simpUser header in a message when a user send a stomp SUBSCRIBE.
Process is as follows: User sends stomp CONNECT, this line is executed: accessor.setUser(principal) and at this point message has properly setsimpUser header. But when stomp SUBSCRIBE message is received in preSend, the StompHeaderAccessor does not have simpUser header. Thus I am not able to asses whether user is already authenticated or not.
So how could I check if a user that sent a SUBSCRIBE message is already authenticated or not ? (and why authenticated user does not send simpUser header)
In your websocket configuration class change your extend to AbstractSecurityWebSocketMessageBrokerConfigurer
then you can set your security as follows:
#Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry message) {
message
.nullDestMatcher().permitAll()
.simpDestMatchers("/app/**").authenticated()
.simpSubscribeDestMatchers("/topics/**").authenticated()
.anyMessage().denyAll();
}
if you get the error Could not verify the provided CSRF token because your session was not found you will have to override `sameOriginDisabled in the websocket configuration class
#Override
protected boolean sameOriginDisabled() {
return true;
}
for more information on websocket security:
https://docs.spring.io/spring-security/site/docs/current/reference/html/websocket.html

Enrich Header at one FTP server and get the header at another FTP server

I have successfully been able send file from one FTP Server(source) to another FTP server (target). I first send files from source to the local directory using the inbound adapter and then send files from the local directory to the target using the outbound adapter. So far this is working fine.
What I want to achieve is: to enrich the header of the message at the source with a hash code (which is generated using the file on source that is transferred) and then get that header at the target and match it with the hash code (which is generated using the file on the target)
Here is what I have tried so far:
Application.java
#SpringBootApplication
public class Application {
#Autowired
private Hashing hashing;
public static ConfigurableApplicationContext context;
public static void main(String[] args) {
context = new SpringApplicationBuilder(Application.class)
.web(false)
.run(args);
}
#Bean
#ServiceActivator(inputChannel = "ftpChannel")
public MessageHandler sourceHandler() {
return new MessageHandler() {
#Override
public void handleMessage(Message<?> message) throws MessagingException {
System.out.println("Reply channel isssss:"+message.getHeaders().getReplyChannel());
Object payload = message.getPayload();
System.out.println("Payload: " + payload);
File file = (File) payload;
// enrich header with hash code before sending to target FTP
Message<?> messageOut = MessageBuilder
.withPayload(message.getPayload())
.copyHeadersIfAbsent(message.getHeaders())
.setHeaderIfAbsent("hashCode", hashing.getHashCode(file)).build();
// send to target FTP
System.out.println("Trying to send " + file.getName() + " to target");
MyGateway gateway = context.getBean(MyGateway.class);
gateway.sendToFtp(messageOut);
}
};
}
}
FileTransferServiceConfig.java
#Configuration
#Component
public class FileTransferServiceConfig {
#Autowired
private ConfigurationService configurationService;
#Autowired
private Hashing hashing;
public static final String FILE_POLLING_DURATION = "5000";
#Bean
public SessionFactory<FTPFile> sourceFtpSessionFactory() {
DefaultFtpSessionFactory sf = new DefaultFtpSessionFactory();
sf.setHost(configurationService.getSourceHostName());
sf.setPort(Integer.parseInt(configurationService.getSourcePort()));
sf.setUsername(configurationService.getSourceUsername());
sf.setPassword(configurationService.getSourcePassword());
return new CachingSessionFactory<>(sf);
}
#Bean
public SessionFactory<FTPFile> targetFtpSessionFactory() {
DefaultFtpSessionFactory sf = new DefaultFtpSessionFactory();
sf.setHost(configurationService.getTargetHostName());
sf.setPort(Integer.parseInt(configurationService.getTargetPort()));
sf.setUsername(configurationService.getTargetUsername());
sf.setPassword(configurationService.getTargetPassword());
return new CachingSessionFactory<>(sf);
}
#MessagingGateway
public interface MyGateway {
#Gateway(requestChannel = "toFtpChannel")
void sendToFtp(Message message);
}
#Bean
public FtpInboundFileSynchronizer ftpInboundFileSynchronizer() {
FtpInboundFileSynchronizer fileSynchronizer = new FtpInboundFileSynchronizer(sourceFtpSessionFactory());
fileSynchronizer.setDeleteRemoteFiles(false);
fileSynchronizer.setRemoteDirectory(configurationService.getSourceDirectory());
fileSynchronizer.setFilter(new FtpSimplePatternFileListFilter(
configurationService.getFileMask()));
return fileSynchronizer;
}
#Bean
public AcceptOnceFileListFilter<File> acceptOnceFileListFilter() {
return new AcceptOnceFileListFilter<>();
}
#Bean
#InboundChannelAdapter(channel = "ftpChannel",
poller = #Poller(fixedDelay = FILE_POLLING_DURATION))
public MessageSource<File> ftpMessageSource() {
FtpInboundFileSynchronizingMessageSource source
= new FtpInboundFileSynchronizingMessageSource(ftpInboundFileSynchronizer());
source.setLocalDirectory(new File(configurationService.getLocalDirectory()));
source.setAutoCreateLocalDirectory(true);
source.setLocalFilter(acceptOnceFileListFilter());
return source;
}
// makes sure transfer continues on connection reset
#Bean
public Advice expressionAdvice() {
ExpressionEvaluatingRequestHandlerAdvice advice = new ExpressionEvaluatingRequestHandlerAdvice();
advice.setTrapException(true);
advice.setOnFailureExpression("#acceptOnceFileListFilter.remove(payload)");
return advice;
}
#Bean
#ServiceActivator(inputChannel = "toFtpChannel")
public void listenOutboundMessage() {
// tried to subscribe to "toFtpChannel" but this was not triggered
System.out.println("Message received");
}
#Bean
#ServiceActivator(inputChannel = "ftpChannel", adviceChain = "expressionAdvice")
public MessageHandler targetHandler() {
FtpMessageHandler handler = new FtpMessageHandler(targetFtpSessionFactory());
handler.setRemoteDirectoryExpression(new LiteralExpression(
configurationService.getTargetDirectory()));
return handler;
}
}
Hashing.java
public interface Hashing {
public String getHashCode(File payload);
}
I have managed to enrich the message in sourceHandler(), built the message and sent it to the target but I cannot figure out how I can receive that message on the target so that I can get the header from the message?
Tell me if any more information is required. I would really appreciate your help.
You have two subscribers on ftpChannel - the target handler and your sourceHandler; they will get alternate messages unless ftpChannel is declared as a pubsub channel.
There should be no problems with your subscription to toFtpChannel.
Turn on DEBUG logging to see all the subscription activity when the application context starts.
EDIT
Remove the #Bean from the #ServiceActivator - such beans must be a MessageHandler.
#ServiceActivator(inputChannel = "toFtpChannel")
public void listenOutboundMessage(Message message) {
// tried to subscribe to "toFtpChannel" but this was not triggered
System.out.println("Message received:" + message);
}
works fine for me...
Payload: /tmp/foo/baz.txt
Trying to send baz.txt to target
Message received:GenericMessage [payload=/tmp/foo/baz.txt, headers={hashCode=foo, id=410eb9a2-fe8b-ea8a-015a-d5896387cf00, timestamp=1509115006278}]
Again; you must have only one subscriber on ftpChannel unless you make it a pubsub.

How to prevent subscription to spring socket /queue/private/* destination.

I have an Java socket API application, that handles socket requests from users and sends responses.
I have a configurer:
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
private static final Logger LOGGER = Logger.getLogger(WebSocketConfig.class);
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/queue");
config.setApplicationDestinationPrefixes("/server_in");
config.setUserDestinationPrefix("/user");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/websocket").withSockJS();
}
}
When i send response to user i do the following:
this.simpMessagingTemplate.convertAndSend("/queue/private/user_"+secret_key, socketResponse);
On client i have the following code:
sc.subscribe('/queue/private/user_'+secret_key, function (greeting) {
console.log(greeting.body);
});
And the response is handled successfully.
But the problem is that some other user can also subscribe to "/queue/private/*" destination and handle private messages.
sc.subscribe('/queue/private/*', function (greeting) {
console.log(greeting.body);
});
How can I privent that behaviour?
If you want each user to have a socket and only get him messages, what you can do is :
Subscribe as you do to the endPoint but with "/user" infront for example
sc.subscribe('/user/queue/websocket, function (greeting) {
console.log(greeting.body);
});
and at the server side you should have a rest method:
#RequestMapping(value = "/test", method = RequestMethod.POST)
public void test(Principal principal) throws Exception {
this.template.convertAndSendToUser(principal.getName(), "/queue/click", "");
}
With this every user subscibers to each channel and only the user is notified about, when a rest call is made.
The rest call should be authenticated so the Principal has the username.
The user channel is auto managed from Spring so you have to add it like this.
You can extend ChannelInterceptorAdapter and manage each event individually:
public class AuthorizedChannelInterceptorAdapter extends ChannelInterceptorAdapter {
#Override
public Message<?> preSend(Message<?> message, MessageChannel messageChannel) throws AuthenticationException {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT == accessor.getCommand())
setUserAuthenticationToken(accessor);
else if (StompCommand.SUBSCRIBE == accessor.getCommand())
validateSubscription((Authentication) accessor.getUser(), accessor.getDestination());
return message;
}
private void setUserAuthenticationToken(StompHeaderAccessor accessor) {
String token = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION);
accessor.setUser(loadAuthentication(token));
}
private Authentication loadAuthentication(String token){
return ....;
}
private void validateSubscription(Authentication authentication, String destination) {
if(...)
throw new AccessDeniedException("No permission to subscribe to this topic");
}
}
First of all you will need to store the authentication object provided by the client in connection event. After this, each event sent by the client will have this authentication object set so you can use it to validate if it is authorized to subscribe to a particular channel.

Categories