Spring Websockets STOMP - get client IP address - stomp

Is there any way to obtain STOMP client IP address? I am intercepting inbound channel but I cannot see any way to check the ip address.
Any help appreciated.

You could set the client IP as a WebSocket session attribute during the handshake with a HandshakeInterceptor:
public class IpHandshakeInterceptor implements HandshakeInterceptor {
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
// Set ip attribute to WebSocket session
attributes.put("ip", request.getRemoteAddress());
return true;
}
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Exception exception) {
}
}
Configure your endpoint with the handshake interceptor:
#Override
protected void configureStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").addInterceptors(new IpHandshakeInterceptor()).withSockJS();
}
And get the attribute in your handler method with a header accessor:
#MessageMapping("/destination")
public void handlerMethod(SimpMessageHeaderAccessor ha) {
String ip = (String) ha.getSessionAttributes().get("ip");
...
}

Below example updated to get the exact remote client ip:
#Component
public class IpHandshakeInterceptor implements HandshakeInterceptor {
#Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
// Set ip attribute to WebSocket session
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
String ipAddress = servletRequest.getServletRequest().getHeader("X-FORWARDED-FOR");
if (ipAddress == null) {
ipAddress = servletRequest.getServletRequest().getRemoteAddr();
}
attributes.put("ip", ipAddress);
}
return true;
}
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Exception exception) {
}
}

Tried to add this information as a comment, but stackoverflow complains that it is too long for the comment, so I post it as answer to
It is possible to get this attribute in service without passing
SimpMessageHeaderAccessor ? I mean something similiar to injecting
HttpServletRequest – shark Nov 23 '15 at 15:02
question.
I was able to achieve "close" results with such syntax:
#MessageMapping("/destination")
#SendTo("/topic/someTopic")
public String send(#Header("simpSessionAttributes") Map<String, Object> sessionAttributes) {
String clientsAddress = sessionAttributes.get("ip"));
return "The sender's address is: " + clientsAddress ;
}
I am not familiar to the origin of the "simpSessionAttributes" name of the header of the message, but I noticed that if I put the information in a way it is described in this thread by the #Sergi Almar - I get such result. But perhaps this name "simpSessionAttributes" may depend on some environment configuration or particular implementation of the message framework idk...
Also I do not mind to include this detalization as a part of the answer of the #Sergi Almar.

Related

Spring Boot - How to get an IP address when using AuthenticationFailureBadCredentialsEvent?

Please tell me how can I get IP address when using AuthenticationFailureBadCredentialsEvent ?
#Autowired
private LoginAttemptService loginAttemptService;
#EventListener
public void onAuthenticationFailure(AuthenticationFailureBadCredentialsEvent event) {
// How can I get the ip address here?
}
The option shown below does not work.
WebAuthenticationDetails auth = (WebAuthenticationDetails) event.getAuthentication();
auth.getRemoteAddress();
I would give a try two possible solutions (I have not tested them) based on the fact that javax.servlet.ServletRequest interface has information about the remote host.
1. Using AuthenticationFailureHandler:
The interface AuthenticationFailureHandler has a method onAuthenticationFailure​(HttpServletRequest, HttpServletResponse, AuthenticationException) that has the request javax.servlet.http.HttpServletRequest available.
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
#Override
public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse res, AuthenticationException e) throws IOException, ServletException {
// implementation
}
}
2. Invoking RequestContextHolder:
This context holder is able to obtain request attributes containing the servlet request currently bound to the thread through the method currentRequestAttributes.
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
ServletRequest servletRequest = attributes.getRequest();

Websocket not working with spring boot application and angular frontend

I looked and tried a lot but I can not find the cause of my problem...
I have a JHipster generated application which consists out of a spring boot application and an angular frontend and I want to use websockets for updates. For that I use Stomp and SockJs
The connection itself is already not working.
I get the following error:
WebSocket connection to 'ws://localhost:9000/updates/websocket/447/xxudq4ni/websocket' failed: WebSocket is closed before the connection is established.
This is the call to port 9000, which is then proxied to the actual backend under Port 8080.
If I call the backend under port 8080 directly, I get:
WebSocket connection to 'ws://localhost:8080/updates/websocket/156/mg0dspp2/websocket' failed: Error during WebSocket handshake: Unexpected response code: 200
I do not really see what the actual response is but I suppose it is the JHIpster error message "an error has occured" and this html is returned with a http statuscode of 200.
I'm out of ideas what the actual problem is... I followed this intro here and several others...
here is my backend:
WebsocketConfig:
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
public static final String IP_ADDRESS = "IP_ADDRESS";
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry
.addEndpoint("/updates/websocket")
//.setHandshakeHandler(defaultHandshakeHandler())
.setAllowedOrigins("*")
.withSockJS()
.setClientLibraryUrl("https://cdn.jsdelivr.net/npm/sockjs-client#1.5.0/dist/sockjs.min.js");
//.setInterceptors(httpSessionHandshakeInterceptor());
}
private DefaultHandshakeHandler defaultHandshakeHandler() {
return new DefaultHandshakeHandler() {
#Override
protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) {
Principal principal = request.getPrincipal();
if (principal == null) {
Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(AuthoritiesConstants.ANONYMOUS));
principal = new AnonymousAuthenticationToken("WebsocketConfiguration", "anonymous", authorities);
}
return principal;
}
};
}
#Bean
public HandshakeInterceptor httpSessionHandshakeInterceptor() {
return new HandshakeInterceptor() {
#Override
public boolean beforeHandshake(
ServerHttpRequest request,
ServerHttpResponse response,
WebSocketHandler wsHandler,
Map<String, Object> attributes
) throws Exception {
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
attributes.put(IP_ADDRESS, servletRequest.getRemoteAddress());
}
return true;
}
#Override
public void afterHandshake(
ServerHttpRequest request,
ServerHttpResponse response,
WebSocketHandler wsHandler,
Exception exception
) {}
};
}
}
Controller:
#Controller
public class UpdateController {
private static final Logger log = LoggerFactory.getLogger(UpdateController.class);
#MessageMapping("/updates/websocket")
#SendTo("/topic/trucks")
public UpdateDto send(UpdateDto dto) {
return dto;
}
}
Frontend:
connect(): void {
if (this.stompClient?.connected || this.called) {
return;
}
this.called = true;
// building absolute path so that websocket doesn't fail when deploying with a context path
let url = '/updates/websocket';
url = this.location.prepareExternalUrl(url);
var socket = new SockJS(url);
this.stompClient = Stomp.over(socket);
this.stompClient.connect({}, (frame) => {
this.connectionSubject.next();
this.sendActivity();
this.routerSubscription = this.router.events
.pipe(filter((event: Event) => event instanceof NavigationEnd))
.subscribe(() => this.sendActivity());
}, error => {
console.log(error);
});
}
Im on Windows and I use Chrome for the development. But it also does not work in FireFox, so I do not think it has something to do with the platform.
Any help would be very much appreciated. Thank you very much!

How to add custom headers to STOMP CREATED message in Spring Boot application?

I'm trying to add custom headers to the STOMP 'CREATED' message, which is received by client at the first connection. Here is the function which connects to the WebSocket using STOMP JavaScript:
function connect() {
socket = new SockJS('/chat');
stompClient = Stomp.over(socket);
stompClient.connect('', '', function(frame) {
whoami = frame.headers['user-name'];
console.log(frame);
stompClient.subscribe('/user/queue/messages', function(message) {
console.log("MESSAGE RECEIVED:");
console.log(message);
showMessage(JSON.parse(message.body));
});
stompClient.subscribe('/topic/active', function(activeMembers) {
showActive(activeMembers);
});
});
}
This function prints the following to the browser's console:
body: ""
command: "CONNECTED"
headers: Object
heart-beat: "0,0"
user-name: "someuser"
version: "1.1"
And i want to add custom header so output must look like:
body: ""
command: "CONNECTED"
headers: Object
heart-beat: "0,0"
user-name: "someuser"
version: "1.1"
custom-header: "foo"
I have the following WebSocket configuration in my Spring Boot app.
WebSocketConfig.java
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/queue", "/topic");
config.setApplicationDestinationPrefixes("/app");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/chat", "/activeUsers")
.withSockJS()
.setInterceptors(customHttpSessionHandshakeInterceptor());
}
...
#Bean
public CustomHttpSessionHandshakeInterceptor
customHttpSessionHandshakeInterceptor() {
return new CustomHttpSessionHandshakeInterceptor();
}
}
I have tried to register the 'HandshakeInterceptor' to set custom header, but it didn't work. Here is 'CustomHttpSessionHandshakeInterceptor':
CustomHttpSessionHandshakeInterceptor.java
public class CustomHttpSessionHandshakeInterceptor implements
HandshakeInterceptor {
#Override
public boolean beforeHandshake(ServerHttpRequest request,
ServerHttpResponse response,
WebSocketHandler wsHandler,
Map<String, Object> attributes) throws Exception {
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletRequest =
(ServletServerHttpRequest) request;
attributes.put("custom-header", "foo");
}
return true;
}
public void afterHandshake(ServerHttpRequest request,
ServerHttpResponse response,
WebSocketHandler wsHandler,
Exception ex) { }
}
I have found this code snippet at https://dzone.com/articles/spring-boot-based-websocket
Can someone explain me why this approach does not work? Is there another way to set custom headers to the STOMP 'CREATED' message at server side in Spring Boot application?
Thanks!
Maybe it's too late, but better late than never ...
Server messages (e.g. CONNECTED) are immutable, means that they cannot be modified.
What I would do is register a client outbound interceptor and trap the connected message by overriding the preSend(...) method and build a new message with my custom headers.
#Override
public Message<?> preSend(Message<?> message, MessageChannel channel)
{
LOGGER.info("Outbound channel pre send ...");
final StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(message);
final StompCommand command = headerAccessor.getCommand();
if (!isNull(command)) {
switch (command) {
case CONNECTED:
final StompHeaderAccessor accessor = StompHeaderAccessor.create(headerAccessor.getCommand());
accessor.setSessionId(headerAccessor.getSessionId());
#SuppressWarnings("unchecked")
final MultiValueMap<String, String> nativeHeaders = (MultiValueMap<String, String>) headerAccessor.getHeader(StompHeaderAccessor.NATIVE_HEADERS);
accessor.addNativeHeaders(nativeHeaders);
// add custom headers
accessor.addNativeHeader("CUSTOM01", "CUSTOM01");
final Message<?> newMessage = MessageBuilder.createMessage(new byte[0], accessor.getMessageHeaders());
return newMessage;
default:
break;
}
}
return message;
}
#UPDATE:::
The interface needed is called ChannelInterceptor and to register your own implementation you need to add #Configuration annotated class
#Configuration
public class CustomMessageBrokerConfig extends WebSocketMessageBrokerConfigurationSupport
implements WebSocketMessageBrokerConfigurer{}
and override a method configureClientOutboundChannel as below
#Override
public void configureClientOutboundChannel(ChannelRegistration registration) {
log.info("Configure client outbound channel started ...");
registration.interceptors(new CustomOutboundChannelInterceptor());
log.info("Configure client outbound channel completed ...");
}
Did you try it like this way? MessageHeaderAccessor has a setHeader method too.
https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-stomp-authentication-token-based

Make simple servlet filter work with #ControllerAdvice

I've a simple filter just to check if a request contains a special header with static key - no user auth - just to protect endpoints. The idea is to throw an AccessForbiddenException if the key does not match which then will be mapped to response with a class annotated with #ControllerAdvice. However I can't make it work. My #ExceptionHandler isn't called.
ClientKeyFilter
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Controller
import javax.servlet.*
import javax.servlet.http.HttpServletRequest
#Controller //I know that #Component might be here
public class ClientKeyFilter implements Filter {
#Value('${CLIENT_KEY}')
String clientKey
public void init(FilterConfig filterConfig) {}
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
req = (HttpServletRequest) req
def reqClientKey = req.getHeader('Client-Key')
if (!clientKey.equals(reqClientKey)) {
throw new AccessForbiddenException('Invalid API key')
}
chain.doFilter(req, res)
}
public void destroy() {}
}
AccessForbiddenException
public class AccessForbiddenException extends RuntimeException {
AccessForbiddenException(String message) {
super(message)
}
}
ExceptionController
#ControllerAdvice
class ExceptionController {
static final Logger logger = LoggerFactory.getLogger(ExceptionController)
#ExceptionHandler(AccessForbiddenException)
public ResponseEntity handleException(HttpServletRequest request, AccessForbiddenException e) {
logger.error('Caught exception.', e)
return new ResponseEntity<>(e.getMessage(), I_AM_A_TEAPOT)
}
}
Where I'm wrong? Can simple servlet filter work with spring-boot's exception mapping?
As specified by the java servlet specification Filters execute always before a Servlet is invoked. Now a #ControllerAdvice is only useful for controller which are executed inside the DispatcherServlet. So using a Filter and expecting a #ControllerAdvice or in this case the #ExceptionHandler, to be invoked isn't going to happen.
You need to either put the same logic in the filter (for writing a JSON response) or instead of a filter use a HandlerInterceptor which does this check. The easiest way is to extend the HandlerInterceptorAdapter and just override and implement the preHandle method and put the logic from the filter into that method.
public class ClientKeyInterceptor extends HandlerInterceptorAdapter {
#Value('${CLIENT_KEY}')
String clientKey
#Override
public boolean preHandle(ServletRequest req, ServletResponse res, Object handler) {
String reqClientKey = req.getHeader('Client-Key')
if (!clientKey.equals(reqClientKey)) {
throw new AccessForbiddenException('Invalid API key')
}
return true;
}
}
You can't use #ControllerAdvice, because it gets called in case of an exception in some controller, but your ClientKeyFilter is not a #Controller.
You should replace the #Controller annotation with the #Component and just set response body and status like this:
#Component
public class ClientKeyFilter implements Filter {
#Value('${CLIENT_KEY}')
String clientKey
public void init(FilterConfig filterConfig) {
}
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
String reqClientKey = request.getHeader("Client-Key");
if (!clientKey.equals(reqClientKey)) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Invalid API key");
return;
}
chain.doFilter(req, res);
}
public void destroy() {
}
}
Servlet Filters in Java classes are used for the following purposes:
To check requests from client before they access resources at backend.
To check responses from server before sent back to the client.
Exception throw from Filter may not be catch by #ControllerAdvice because in may not reach DispatcherServlet. I am handling in my project as below:
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws IOException, ServletException {
String token = null;
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && (bearerToken.contains("Bearer "))) {
if (bearerToken.startsWith("Bearer "))
token = bearerToken.substring(7, bearerToken.length());
try {
AuthenticationInfo authInfo = TokenHandler.validateToken(token);
logger.debug("Found id:{}", authInfo.getId());
authInfo.uri = request.getRequestURI();
AuthPersistenceBean persistentBean = new AuthPersistenceBean(authInfo);
SecurityContextHolder.getContext().setAuthentication(persistentBean);
logger.debug("Found id:'{}', added into SecurityContextHolder", authInfo.getId());
} catch (AuthenticationException authException) {
logger.error("User Unauthorized: Invalid token provided");
raiseException(request, response);
return;
} catch (Exception e) {
raiseException(request, response);
return;
}
// Wrapping the error response
private void raiseException(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
ApiError apiError = new ApiError(HttpStatus.UNAUTHORIZED);
apiError.setMessage("User Unauthorized: Invalid token provided");
apiError.setPath(request.getRequestURI());
byte[] body = new ObjectMapper().writeValueAsBytes(apiError);
response.getOutputStream().write(body);
}
// ApiError class
public class ApiError {
// 4xx and 5xx
private HttpStatus status;
// holds a user-friendly message about the error.
private String message;
// holds a system message describing the error in more detail.
private String debugMessage;
// returns the part of this request's URL
private String path;
public ApiError(HttpStatus status) {
this();
this.status = status;
}
//setter and getters

Modify request URI in spring mvc

I have a spring mvc based application. I want to modify the request URI before it reaches controller. For example, RequestMapping for controller is "abc/xyz" but the request coming is "abc/1/xyz". I want to modify incoming request to map it to controller.
Solution1: Implement interceptor and modify incoming request URI. But the problem here is that as there is no controller matching the URI pattern "abc/1/xyz", it does not even goes to interceptor.(I might be missing something to enable it if its there)
Get around for it could be to have both of URI as request mapping for controller.
What other solutions could be there? Is there a way to handle this request even before it comes to spring. As in handle it at filter in web.xml, i am just making it up.
You could write a servlet Filter which wraps the HttpServletRequest and returns a different value for the method getRequestURI. Something like that:
public class RequestURIOverriderServletFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
chain.doFilter(new HttpServletRequestWrapper((HttpServletRequest) request) {
#Override
public String getRequestURI() {
// return what you want
}
}, response);
}
// ...
}
The servlet filter configuration must be added into the web.xml.
But sincerly, there is probably other way to solve your problems and you should not do this unless you have very good reasons.
in order to achieve this you should replace every place that affected when you calling uri.
the place that not mentioned is INCLUDE_SERVLET_PATH_ATTRIBUTE which is internally is accessed when going deeper.
public class AuthFilter implements Filter {
private final Logger logger = LoggerFactory.getLogger(AuthFilter.class);
private final String API_PREFIX = "/api";
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String requestURI = httpRequest.getRequestURI();
if (requestURI.startsWith(API_PREFIX)) {
String redirectURI = requestURI.substring(API_PREFIX.length());
StringBuffer redirectURL = new StringBuffer(((HttpServletRequest) request).getRequestURL().toString().replaceFirst(API_PREFIX, ""));
filterChain.doFilter(new HttpServletRequestWrapper((HttpServletRequest) request) {
#Override
public String getRequestURI() {
return redirectURI;
}
#Override
public StringBuffer getRequestURL() {
return redirectURL;
}
#Override
public Object getAttribute(String name) {
if(WebUtils.INCLUDE_SERVLET_PATH_ATTRIBUTE.equals(name))
return redirectURI;
return super.getAttribute(name);
}
}, response);
} else {
filterChain.doFilter(request, response);
}
}
}
You can use a URL Re-Write which are specifically meant for this purpose i.e. transform one request URI to another URI based on some regex.

Resources