My aim is to connect browser clients having proper headers with the server. I pass these headers from StompClient.
My UI code in which i passed token in the header is
function connect() {
var socket = new SockJS('/websocket/api/add');
stompClient = Stomp.over(socket);
stompClient.connect({"token" : "12345"}, function(frame) {
setConnected(true);
console.log('Connected: ' + frame);
});
}
In backend i am able to read the headers in the preSend() method of ChannelInterceptorAdapter
#Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
MessageHeaders headers = message.getHeaders();
System.out.println("preSend : HEADERS : {}" + headers);
return super.preSend(message, channel);
}
But here i am not able to close the wesocket session. How can we do that?
Also i was able to close the websocket session but i couldn't receive the headers in afterConnectionEstablished() method of WebSocketHandlerDecorator
public void configureWebSocketTransport(final WebSocketTransportRegistration registration) {
registration.addDecoratorFactory(new WebSocketHandlerDecoratorFactory() {
#Override
public WebSocketHandler decorate(final WebSocketHandler handler) {
return new WebSocketHandlerDecorator(handler) {
#Override
public void afterConnectionEstablished(final WebSocketSession session) throws Exception {
session.close(CloseStatus.NOT_ACCEPTABLE);
super.afterConnectionEstablished(session);
}
};
}
});
super.configureWebSocketTransport(registration);
}
Can someone guide me how can i close the websocketsession based on the header we pass from UI at server side?
You can try sending the client's token to the server through a message when connection established, then let the server save that session into a map, whose key is the corresponding token.
So when you want to close a session by its token, you can query for the session from that map using the token.
Sample Code:
Save the session with its token:
#Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
String messageToString = message.getPayload().toString();
if (messageToString.startsWith("token=")) {
tokenToSessionMapping.put(messageToString.substring("token=".length()));
}
// Other handling message code...
}
Close the session by token:
WebSocketSession sessionByToken = tokenToSessionMapping.get(token);
if (sessionByToken != null && sessionByToken.isOpen()) {
sessionByToken.close(CloseStatus.NOT_ACCEPTABLE);
}
And other things to notice:
Since the tokenToSessionMapping is static and is shared among sessions. You should use a thread-safe implementation such as ConcurrentHashMap.
When the session is closed, you'd better remove the corresponding entry from the map tokenToSessionMapping. Otherwise the map size will just keep growing. You can do this by the override method afterConnectionClosed().
#Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
Log.info("Socket session closed: {}", status.toString());
String foundKey = null;
for (Map.Entry<String, String> entry : tokenToSessionMapping.entrySet()) {
if (Objects.equals(entry.getValue(), session)) {
foundKey = entry.getKey();
}
}
if (foundKey != null) {
tokenToSessionMapping.remove(foundKey);
}
}
Related
I'm new in Spring and I trying log in to chat application with spring security username.
But controller or stomp don't recognize input. And don't allows to enter chat
Where is I should to write in stomp or controller?
Thank you.
Chat Controller
#Controller
public class ChatController {
#GetMapping("/chat")
public String chat() {
return "chat";
}
#MessageMapping("/chat.sendMessage")
#SendTo("/topic/public")
public ChatMessagePojo sendMessage(#Payload ChatMessagePojo chatMessagePojo) {
return chatMessagePojo;
}
#MessageMapping("/chat.addUser")
#SendTo("/topic/public")
public ChatMessagePojo addUser(#Payload ChatMessagePojo chatMessagePojo, SimpMessageHeaderAccessor headerAccessor) {
// Add username in web socket session
headerAccessor.getSessionAttributes().put("email", chatMessagePojo.getSender());
return chatMessagePojo;
}
ChatMessage class
public class ChatMessagePojo {
private MessageType type;
private String content;
private String sender;
public enum MessageType {
CHAT,
JOIN,
LEAVE
}
}
Stomp
const connect = () => {
const Stomp = require("stompjs");
var SockJS = require("sockjs-client");
SockJS = new SockJS("http://localhost:8080/ws");
stompClient = Stomp.over(SockJS);
stompClient.connect({}, onConnected, onError);
};
const onConnected = () => {
console.log("connected");
stompClient.subscribe(
"/user/" + currentUser.id + "/queue/messages",
onMessageReceived
);
};
const sendMessage = (msg) => {
if (msg.trim() !== "") {
const message = {
senderId: currentUser.id,
recipientId: activeContact.id,
senderName: currentUser.name,
recipientName: activeContact.name,
content: msg,
timestamp: new Date(),
};
stompClient.send("/app/chat", {}, JSON.stringify(message));
}
};
Example login screen to chat:
I tried but failed, and then I solved it with my own solution. Maybe you can refer to it.
Make a socket endpoint for authentication and send the ID and password for authentication.
Save id and session after successful authentication.
Then get the user id through the session map SESSION_USERID.
Frontend
stompClient.send("/app/chat/login", {}, "{type: AUTH, data: {id: 'mock', pwd: '12345678'}}");
Backend
public static final Map<String, Long> SESSION_USERID = new ConcurrentHashMap<>();
#Override
public void handleTextMessage(WebSocketSession session, TextMessage message)
// parse message to object
if (message.type == AUTH) {
// do auth
// if auth succeed, cache the user
SESSION_USERID.put(userId, session.getId());
} else if (message.type == CHAT) {
Long userId = SESSION_USERID.get(session.getId());
if (userId == null) {
// Send error or close session
} else {
// Send messages to other peoples
}
}
}
//----
Is there a way to enable encoded slashes for websockets in tomcat?
I have set org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH to true and it works as expected for web requests.
Hovewer, there is a problem with WS requests. That setting doesn't seem to work for them.
I have WS endpoint configured as like this:
#ServerEndpoint(value = "/ws/data/{path}", decoders = ChatMessageDecoder.class, encoders = ChatMessageEncoder.class)
It works just fine for URL ws://localhost:8080/app/ws/data/Test but it doesn't work for URL ws://localhost:8080/ws/data/Test%2FSubDir. For the latter URL I receive 404 error.
Is there a way to allow encoded slashes for WS requests?
Maybe you are ignoring the #PathParam.
I tried the following code and works as expected without any additional configuration:
package com.logicbig.example;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import javax.websocket.server.PathParam;
#ServerEndpoint("/hello/{parameter}")
public class HelloWorldEndpointWilcard {
public HelloWorldEndpointWilcard() {
System.out.println("class loaded " + this.getClass());
}
#OnOpen
public void onOpen(Session session, #PathParam("parameter") String parameter) {
System.out.printf("Session opened, id: %s%n", session.getId());
System.out.println("parameter onOpen:"+parameter);
try {
session.getBasicRemote().sendText("Hi there, we are successfully connected.");
} catch (IOException ex) {
ex.printStackTrace();
}
}
#OnMessage
public void onMessage(String message, Session session, #PathParam("parameter") String parameter) {
System.out.printf("Message received. Session id: %s Message: %s%n",
session.getId(), message);
System.out.println("parameter onMessage:"+parameter);
try {
session.getBasicRemote().sendText(String.format("We received your message: %s%n", message));
} catch (IOException ex) {
ex.printStackTrace();
}
}
#OnError
public void onError(Throwable e) {
e.printStackTrace();
}
#OnClose
public void onClose(Session session) {
System.out.printf("Session closed with id: %s%n", session.getId());
}
}
And this javascript client:
var webSocket = new WebSocket("ws://localhost:8080/example/hello/Test%2FSubDir");
Test%2FSubDir value was received successfully on session established and on each message:
Complete java source code here
I couldn't make encoding working, but I have 3 alternative solutions.
Make multiple #ServerEndpoint:
#ServerEndpoint(value = "/ws/data/{path}"
public SingleChatHandler : ChatHandler {
#OnOpen
public void onOpen(
Session session,
#PathParam("path") String path) {
onOpenInternal(session, path);
}
}
#ServerEndpoint(value = "/ws/data/{path1}/{path2}"
public DoubleChatHandler : ChatHandler {
#OnOpen
public void onOpen(
Session session,
#PathParam("path1") String path1,
#PathParam("path2") String path2) {
onOpenInternal(session, path1 + '/' + path2);
}
}
public abstract class ChatHandler {
protected void onOpenInternal(Session session, String path) { [...] }
}
This however has two main disadvantages:
depth of the path must be know beforehand to implement required handlers
there is no way to have more than one parameter with slashes
Use query string instead of #PathParam:
#ServerEndpoint(value = "/ws/data")
public class ChatHandler {
#OnOpen
public void onOpen(Session session) {
Map<String, List<String>> map = session.getRequestParameterMap();
if (!map.containsKey("path")) {
// TODO: throw exception
}
String path = map.get("path").get(0);
}
}
This has 2 disadvantages as well, but less troublesome:
URL is not too nice
there will be no 404 when parameters are not provided (I still see 101)
Use query string instead of #PathParam (use code from 2. as a base) and check query string in modifyHandshake method:
public class ChatHandlerConfigurator extends ServerEndpointConfig.Configurator
{
#Override
public void modifyHandshake(ServerEndpointConfig config, HandshakeRequest request, HandshakeResponse response)
{
Map<String, List<String>> map = request.getParameterMap();
if (!map.containsKey("path")) {
throw new IllegalArgumentException("path was not provided");
}
// Optionally add values to userProperties
Map<String, Object> userProperties = config.getUserProperties();
userProperties.put("path", map.get("path").get(0));
}
}
This behaves similarly to 2. but since I throw exception on handshake, instead of 101 there will be 500 error.
public WebSocket connect() throws Exception {
WebSocket ws = new WebSocketFactory().setConnectionTimeout(TIMEOUT).createSocket(SERVER)
.addListener(new WebSocketAdapter() {
public void onTextMessage(WebSocket websocket, String message) {
System.out.println(message);
}
public void onConnected(WebSocket websocket, java.util.Map headers) {
System.out.println(websocket.getSocket().toString());
System.out.println(headers.toString());
logger.logInfo("QlikWebConnector", "connect", "connection: ",
websocket.getSocket().toString() + headers.toString());
}
public void onMessageError(com.neovisionaries.ws.client.WebSocket websocket,
com.neovisionaries.ws.client.WebSocketException cause, java.util.List frames)
throws java.lang.Exception {
}
public void onSendError(com.neovisionaries.ws.client.WebSocket websocket,
com.neovisionaries.ws.client.WebSocketException cause,
com.neovisionaries.ws.client.WebSocketFrame frame) throws java.lang.Exception {
}
public void onBinaryMessage(com.neovisionaries.ws.client.WebSocket websocket, byte[] binary)
throws java.lang.Exception {
}
public void onDisconnected(com.neovisionaries.ws.client.WebSocket websocket,
com.neovisionaries.ws.client.WebSocketFrame serverCloseFrame,
com.neovisionaries.ws.client.WebSocketFrame clientCloseFrame, boolean closedByServer)
throws java.lang.Exception {
}
}).addExtension(WebSocketExtension.PERMESSAGE_DEFLATE).connect();
if (ws.isOpen()) {
System.out.println("websocket is open ");
logger.logInfo("QlikWebConnector", "connect", "websocket is open: ", "websocket is open");
}
return ws;
}
I am sending request like:-
1) ws.sendText("my message1") 2)ws.sendText("my message2")
What is happening?
1) both requests get fired at the same time.
Problem/Expected:-
1) I want my socket to wait till first request get processed as my second request is dependent on response of first request.
Any leads?
One way you can do that is to change the logic of your program a little. For example, instead of sending both messages at the same time and from the same place, you can send the first message and listen for the response. After getting a response for the first message - you can send the second one.
Something like this:
public void onTextMessage(WebSocket websocket, String message) {
if (shouldSendSeccondMessage(message)) {
sendSeccondMessage();
}
}
If you have some state and according to the state you send a particular message - then you can think about using the State pattern. So, for each message, your state machine will decide the next step(message).
The Spring framework support tcp connection as well , i wrote code below to setup a simple socket server , i am confused about adding below futures to my socket server :
authorizing clients based on a unique identifier ( for example a client secret received from client, maybe using TCP Connection Events )
send a message directly to specific client (based on identifier)
broadcast a message
UPDATE :
Config.sendMessage added to send message to single client
Config.broadCast added to broadcast message
authorizeIncomingConnection to authorize clients , accept or reject connections
tcpConnections static filed added to keep tcpEvent sources
Questions !
is using tcpConnections HashMap good idea ?!
is the authorization method i implemented a good one ?!
Main.java
#SpringBootApplication
public class Main {
public static void main(final String[] args) {
SpringApplication.run(Main.class, args);
}
}
Config.java
#EnableIntegration
#IntegrationComponentScan
#Configuration
public class Config implements ApplicationListener<TcpConnectionEvent> {
private static final Logger LOGGER = Logger.getLogger(Config.class.getName());
#Bean
public AbstractServerConnectionFactory AbstractServerConnectionFactory() {
return new TcpNetServerConnectionFactory(8181);
}
#Bean
public TcpInboundGateway TcpInboundGateway(AbstractServerConnectionFactory connectionFactory) {
TcpInboundGateway inGate = new TcpInboundGateway();
inGate.setConnectionFactory(connectionFactory);
inGate.setRequestChannel(getMessageChannel());
return inGate;
}
#Bean
public MessageChannel getMessageChannel() {
return new DirectChannel();
}
#MessageEndpoint
public class Echo {
#Transformer(inputChannel = "getMessageChannel")
public String convert(byte[] bytes) throws Exception {
return new String(bytes);
}
}
private static ConcurrentHashMap<String, TcpConnection> tcpConnections = new ConcurrentHashMap<>();
#Override
public void onApplicationEvent(TcpConnectionEvent tcpEvent) {
TcpConnection source = (TcpConnection) tcpEvent.getSource();
if (tcpEvent instanceof TcpConnectionOpenEvent) {
LOGGER.info("Socket Opened " + source.getConnectionId());
tcpConnections.put(tcpEvent.getConnectionId(), source);
if (!authorizeIncomingConnection(source.getSocketInfo())) {
LOGGER.warn("Socket Rejected " + source.getConnectionId());
source.close();
}
} else if (tcpEvent instanceof TcpConnectionCloseEvent) {
LOGGER.info("Socket Closed " + source.getConnectionId());
tcpConnections.remove(source.getConnectionId());
}
}
private boolean authorizeIncomingConnection(SocketInfo socketInfo) {
//Authorization Logic , Like Ip,Mac Address WhiteList or anyThing else !
return (System.currentTimeMillis() / 1000) % 2 == 0;
}
public static String broadCast(String message) {
Set<String> connectionIds = tcpConnections.keySet();
int successCounter = 0;
int FailureCounter = 0;
for (String connectionId : connectionIds) {
try {
sendMessage(connectionId, message);
successCounter++;
} catch (Exception e) {
FailureCounter++;
}
}
return "BroadCast Result , Success : " + successCounter + " Failure : " + FailureCounter;
}
public static void sendMessage(String connectionId, final String message) throws Exception {
tcpConnections.get(connectionId).send(new Message<String>() {
#Override
public String getPayload() {
return message;
}
#Override
public MessageHeaders getHeaders() {
return null;
}
});
}
}
MainController.java
#Controller
public class MainController {
#RequestMapping("/notify/{connectionId}/{message}")
#ResponseBody
public String home(#PathVariable String connectionId, #PathVariable String message) {
try {
Config.sendMessage(connectionId, message);
return "Client Notified !";
} catch (Exception e) {
return "Failed To Notify Client , cause : \n " + e.toString();
}
}
#RequestMapping("/broadCast/{message}")
#ResponseBody
public String home(#PathVariable String message) {
return Config.broadCast(message);
}
}
Usage :
Socket Request/Response Mode
notify single client
http://localhost:8080/notify/{connectionId}/{message}
broadCast
http://localhost:8080/broadCast/{message}
The TcpConnectionOpenEvent contains a connectionId property. Each message coming from that client will have the same property in the IpHeaders.CONNECTION_ID message header.
Add a custom router that keeps track of the logged-on state of each connection.
Lookup the connection id and if not authenticated, route to a challenge/response subflow.
When authenticated, route to the normal flow.
To use arbitrary messaging (rather than request/response) use a TcpReceivingChannelAdapter and TcpSendingMessageHandler instead of an inbound gateway. Both configured to use the same connection factory. For each message sent to the message handler, add the IpHeaders.CONNECTION_ID header to target the specific client.
To broadcast, send a message for each connection id.
I have just started netty and I am really disappointed with the documentation present on
their website.
I am trying to connect to an URL using Netty.. I took the time client example from their website and changed it as per my requirement..
Code :
public class NettyClient {
public static void main(String[] args) throws Exception {
String host = "myUrl.com/v1/parma?param1=value";
int port = 443;
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(workerGroup);
b.channel(NioSocketChannel.class);
b.option(ChannelOption.SO_KEEPALIVE, true);
b.handler(new ChannelInitializer<SocketChannel>() {
#Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ClientHandler());
ch.pipeline().addLast("encoder", new HttpRequestEncoder());
}
});
// Start the client.
ChannelFuture f = b.connect(host, port).sync();
// Wait until the connection is closed.
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
}
}
}
But the problem is that that it expects only the url without the query parameters.. How can I pass query parameters with the URL?
and please provide me some link of a good documentation for Netty 4..
EDIT
Client code after referring the example mentioned in the answer :
URI uri = new URI("myUrl.com/v1/parma?param1=value");
String scheme = uri.getScheme() == null? "http" : uri.getScheme();
String host = "myUrl.com";
int port = 443;
boolean ssl = "https".equalsIgnoreCase(scheme);
// Configure the client.
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.handler(new NettyClientInitializer(ssl));
// Make the connection attempt.
Channel ch = b.connect(host, port).sync().channel();
// Prepare the HTTP request.
HttpRequest request = new DefaultHttpRequest(
HttpVersion.HTTP_1_1, HttpMethod.GET, uri.getRawPath());
request.headers().set(HttpHeaders.Names.HOST, host);
request.headers().set(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.CLOSE);
//request.headers().set(HttpHeaders.Names.ACCEPT_ENCODING, HttpHeaders.Values.GZIP);
/*// Set some example cookies.
request.headers().set(
HttpHeaders.Names.COOKIE,
ClientCookieEncoder.encode(
new DefaultCookie("my-cookie", "foo"),
new DefaultCookie("another-cookie", "bar")));
*/
// Send the HTTP request.
ch.writeAndFlush(request);
// Wait for the server to close the connection.
ch.closeFuture().sync();
} finally {
// Shut down executor threads to exit.
group.shutdownGracefully();
}
handler code :
public class ClientHandler extends SimpleChannelInboundHandler<HttpObject> {
#Override
public void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
if (msg instanceof HttpResponse) {
HttpResponse response = (HttpResponse) msg;
System.out.println("STATUS: " + response.getStatus());
System.out.println("VERSION: " + response.getProtocolVersion());
System.out.println();
if (!response.headers().isEmpty()) {
for (String name: response.headers().names()) {
for (String value: response.headers().getAll(name)) {
System.out.println("HEADER: " + name + " = " + value);
}
}
System.out.println();
}
if (HttpHeaders.isTransferEncodingChunked(response)) {
System.out.println("CHUNKED CONTENT {");
} else {
System.out.println("CONTENT {");
}
}
if (msg instanceof HttpContent) {
HttpContent content = (HttpContent) msg;
System.out.print(content.content().toString(CharsetUtil.UTF_8));
System.out.flush();
if (content instanceof LastHttpContent) {
System.out.println("} END OF CONTENT");
}
}
}
#Override
public void exceptionCaught(
ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
initializer code :
public class NettyClientInitializer extends ChannelInitializer<SocketChannel> {
private final boolean ssl;
public NettyClientInitializer(boolean ssl) {
this.ssl = ssl;
}
#Override
public void initChannel(SocketChannel ch) throws Exception {
// Create a default pipeline implementation.
ChannelPipeline p = ch.pipeline();
p.addLast("log", new LoggingHandler(LogLevel.INFO));
// Enable HTTPS if necessary.
/*
if (ssl) {
SSLEngine engine =
SecureChatSslContextFactory.getClientContext().createSSLEngine();
engine.setUseClientMode(true);
p.addLast("ssl", new SslHandler(engine));
}
*/
p.addLast("codec", new HttpClientCodec());
// Remove the following line if you don't want automatic content decompression.
// p.addLast("inflater", new HttpContentDecompressor());
// Uncomment the following line if you don't want to handle HttpChunks.
p.addLast("aggregator", new HttpObjectAggregator(1048576));
p.addLast("handler", new ClientHandler());
}
}
Your code only handles the low-level connection at the moment. Indeed at this level only the hostname and port can be used.
For the HTTP request You have to construct an HttpRequest object and send it over the channel. In this request object You define the query parameters and all such things.
There is a bunch of example code about HTTP client functionality on Netty website - have a a look!
In this example the problem lies with the constructor for the DefaultHttpRequest parameter of uri.getRawPath(). The invocation of this method does NOT return the query parameters. It works in this case as there were no query parameters in the Snoop example. By substituting uri.toASCIIString() returns the encoded uri complete with the query parameters. To prove this to yourself, rather than having a method invocation within a method invocation (a bad idea for just this reason, add the statement
String url = uri.getRawPath();
and look at the string url.
I had the exact same problem. I've done this natively in servlets for years but now was trying to do it in a Netty app.
Consequently the new code would be:
String path = uri.toASCIIString();
// Prepare the HTTP request.
HttpRequest request = new DefaultFullHttpRequest(
HttpVersion.HTTP_1_1, HttpMethod.GET, path);
When you build the request, you need to add the query to the path. Instead of
uri.getRawPath()
use
uri.getRawPath() + "?" + uri.getRawQuery()