Add dynamically websocket connection via REST endpoint in spring boot application - java

I have implemented WebSocket mechanism in spring boot application.
#Configuration
#EnableWebSocket
public class WebSocketConfiguration implements WebSocketConfigurer {
private String webSockerClientUri;
private WebSocketHandlerRegistry webSocketHandlerRegistry;
private WebSocketHandler webSocketHandler;
#Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
this.webSocketHandlerRegistry = webSocketHandlerRegistry;
this.webSocketHandler = registerWebSocketHandler();
this.webSocketHandlerRegistry.addHandler(this.webSocketHandler, "/test");
}
public WebSocketHandler registerWebSocketHandler(){
return new WebSocketHandler();
}
public WebSocketHandlerRegistry getWebSocketHandlerRegistry() {
return webSocketHandlerRegistry;
}
public WebSocketHandler getWebSocketHandler() {
return webSocketHandler;
}
}
During startup appp WebSocketHandlerMapping detect registration of Websocket connection "/test". I would like to do the same action in REST endpoint like this.
#PostMapping("/{uri}")
public ResponseEntity register(#PathVariable("uri") String uri){
if (uri != null){
this.webSocketConfiguration.getWebSocketHandlerRegistry().addHandler(webSocketConfiguration.getWebSocketHandler(), "/" + uri);
return new ResponseEntity(HttpStatus.OK);
}
else {
return new ResponseEntity(HttpStatus.NO_CONTENT);
}
}
Uri is added into WebSocketHandlerRegistry however mechanism WebSocketHandlerMapping doesn't detect that fact, because it is not startup state of app. How can I solve this problem?

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.

How to retrieve the current logged in user in a websocket controller

I am trying to obtain the currently authenticated user in the controller for websockets. The problem is, I cannot access the user using SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getId().
I have tried to give Principal as a parameter to the method but it returns principal null.
Security configuration:
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/connect").setAllowedOrigins("*");
}
#Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic/messages");
registry.setApplicationDestinationPrefixes("/ws");
}
}
Controller for websocket:
#Controller
public class MessageController {
#Autowired
private Consumer consumer;
#Autowired
private Utils utils;
#Autowired
private PersonService personService;
#Autowired
SimpMessagingTemplate simpMessagingTemplate;
String destination = "/topic/messages";
ExecutorService executorService =
Executors.newFixedThreadPool(1);
Future<?> submittedTask;
#MessageMapping("/start")
public void startTask(Principal principal){
// Here, I would like to get the logged in user
// If I use principal like this: principal.getName() => NullPointerException
if ( submittedTask != null ){
simpMessagingTemplate.convertAndSend(destination,
"Task already started");
return;
}
simpMessagingTemplate.convertAndSendToUser(sha.getUser().getName(), destination,
"Started task");
submittedTask = executorService.submit(() -> {
while(true){
simpMessagingTemplate.convertAndSend(destination,
// "The calculated value " + val + " is equal to : " + max);
}
});
}
How can I get the authenticated user? I needed it to check when to start the task for the web socket
Try to implement ChannelInterceptor, that need to be registrated in Config file (class that implements WebSocketMessageBrokerConfigurer)
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
private final ChannelInterceptor serverPushInBoundInterceptor;
#Autowired
public WebSocketConfig(#Qualifier("serverPushInBoundInterceptor") ChannelInterceptor serverPushInBoundInterceptor) {
this.serverPushInBoundInterceptor = serverPushInBoundInterceptor;
}
....
#Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(serverPushInBoundInterceptor);
}
}
#Component("serverPushInBoundInterceptor")
public class ServerPushInBoundInterceptor implements ChannelInterceptor {
private static final Logger log = LoggerFactory.getLogger(ServerPushInBoundInterceptor.class);
#Override
#SuppressWarnings("NullableProblems")
public Message<?> postReceive(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(Objects.requireNonNull(accessor).getCommand())) {
List<String> authorization = accessor.getNativeHeader("Authorization");
if (authorization != null && !authorization.isEmpty()) {
String auth = authorization.get(0).split(" ")[1];
System.out.println(auth);
try {
// find Principal
Principal principal = ...
accessor.setUser(new UsernamePasswordAuthenticationToken(principal, principal.getCredentials(), principal.getAuthorities()));
} catch (Exception exc) {
log.error("preSend", exc);
}
}
}
return message;
}
}

Spring boot's convertAndSendToUser not working

I'm new to Spring boot and trying to learn it. I'm following a tutorial on how to make a simple one-to-one chat app.
Everything is working fine except that messages aren't getting sent between users. Messages get from the sender to the server but when convertAndSentToUser() nothing gets to the other client (recipient).
Here's the message broker configuration:
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketMessageBrokerConfig implements WebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/user");
config.enableSimpleBroker("/topic", "/queue");
config.setApplicationDestinationPrefixes("/app");
config.setUserDestinationPrefix("/user");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/broadcast");
registry.addEndpoint("/broadcast").withSockJS().setHeartbeatTime(60_000);
registry.addEndpoint("/chat").withSockJS();
}
#Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new UserInterceptor());
}
}
And the controller that handles new connections:
#RestController
#Log4j2
public class WebSocketConnectionRestController {
#Autowired
private ActiveUserManager activeSessionManager;
...
#PostMapping("/rest/user-connect")
public String userConnect(HttpServletRequest request, #ModelAttribute("username") String userName) {
String remoteAddr = "";
if (request != null) {
remoteAddr = request.getHeader("Remote_Addr");
if (remoteAddr == null || remoteAddr.isEmpty()) {
remoteAddr = request.getHeader("X-FORWARDED-FOR");
if (remoteAddr == null || "".equals(remoteAddr)) {
remoteAddr = request.getRemoteAddr();
}
}
}
log.info("registered " + userName + " " + remoteAddr);
activeSessionManager.add(userName, remoteAddr);
return remoteAddr;
}
...
}
Finally, here's the controller that handles new messages:
#Controller
#Log4j2
public class WebSocketChatController implements ActiveUserChangeListener {
#Autowired
private SimpMessagingTemplate webSocket;
...
#MessageMapping("/chat")
public void send(#Payload ChatMessage chatMessage) throws Exception {
ChatMessage message = new ChatMessage(chatMessage.getFrom(), chatMessage.getText(), chatMessage.getRecipient());
log.info("sent message to " + chatMessage.getRecipient());
webSocket.convertAndSendToUser(chatMessage.getRecipient(), "/queue/messages", message);
}
...
}
I finally found the problem. The tutorial had a UserInterceptor which links the session id for new connections to the username but it was trying to cast an Arraylist to a LinkedList so I just casted it to a List instead:
public class UserInterceptor implements ChannelInterceptor {
#Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor
= MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
Object raw = message
.getHeaders()
.get(SimpMessageHeaderAccessor.NATIVE_HEADERS);
if (raw instanceof Map) {
Object name = ((Map) raw).get("username");
if (name instanceof List) {
accessor.setUser(new User(((List) name).get(0).toString()));
}
}
}
return message;
}
}

Enable logging in spring boot actuator health check API

I am using Spring boot Actuator API for my project the having a health check endpoint, and enabled it by :
management.endpoints.web.base-path=/
management.endpoints.web.path-mapping.health=healthcheck
Mentioned here
Now I want to enable log in my application log file when ever the status of this above /healthcheck fails and print the entire response from this end point.
What is the correct way to achieve this?
Best way is to extend the actuator endpoint with #EndpointWebExtension. You can do the following;
#Component
#EndpointWebExtension(endpoint = HealthEndpoint.class)
public class HealthEndpointWebExtension {
private HealthEndpoint healthEndpoint;
private HealthStatusHttpMapper statusHttpMapper;
// Constructor
#ReadOperation
public WebEndpointResponse<Health> health() {
Health health = this.healthEndpoint.health();
Integer status = this.statusHttpMapper.mapStatus(health.getStatus());
// log here depending on health status.
return new WebEndpointResponse<>(health, status);
}
}
More about actuator endpoint extending here, at 4.8. Extending Existing Endpoints
The above answers did not work for me. I implemented the below and it works. When you view [myhost:port]/actuator/health from your browser the below will execute. You can also add healthCheckLogger to your readiness/liveness probes so it executes periodically.
#Slf4j
#Component
public class HealthCheckLogger implements HealthIndicator
{
#Lazy
#Autowired
private HealthEndpoint healthEndpoint;
#Override
public Health health()
{
log.info("DB health: {}", healthEndpoint.healthForPath("db"));
log.info("DB health: {}", healthEndpoint.healthForPath("diskSpace"));
return Health.up().build();
}
}
Extending the HealthEndpoint using a EndpointWebExtension does not work with newer Spring versions. It's not allowed to override the existing (web-) extension or re-register another one.
Another solution is using a Filter. The following implementation logs if the health check fails:
public class HealthLoggingFilter implements Filter {
private static final Logger LOG = LoggerFactory.getLogger(HealthLoggingFilter.class);
#Override
public void init(FilterConfig filterConfig) {
// nothing to do
}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
ContentCachingResponseWrapper responseCacheWrapperObject = new ContentCachingResponseWrapper((HttpServletResponse) response);
chain.doFilter(request, responseCacheWrapperObject);
int status = ((HttpServletResponse) response).getStatus();
if (status >= 400) { // unhealthy
byte[] responseArray = responseCacheWrapperObject.getContentAsByteArray();
String responseStr = new String(responseArray, responseCacheWrapperObject.getCharacterEncoding());
LOG.warn("Unhealthy. Health check returned: {}", responseStr);
}
responseCacheWrapperObject.copyBodyToResponse();
}
#Override
public void destroy() {
// nothing to do
}
}
The Filter can be registered for the actuator/health route using FilterRegistrationBean:
#Bean
public FilterRegistrationBean<HealthLoggingFilter > loggingFilter(){
FilterRegistrationBean<HealthLoggingFilter > registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new HealthLoggingFilter ());
registrationBean.addUrlPatterns("/actuator/health");
return registrationBean;
}
If using Webflux this worked for me sample in Kotlin
#Component
#EndpointWebExtension(endpoint = HealthEndpoint::class)
class LoggingReactiveHealthEndpointWebExtension(
registry: ReactiveHealthContributorRegistry,
groups: HealthEndpointGroups
) : ReactiveHealthEndpointWebExtension(registry, groups) {
companion object {
private val logger = LoggerFactory.getLogger(LoggingReactiveHealthEndpointWebExtension::class.java)
}
override fun health(
apiVersion: ApiVersion?,
securityContext: SecurityContext?,
showAll: Boolean,
vararg path: String?
): Mono<WebEndpointResponse<out HealthComponent>> {
val health = super.health(apiVersion, securityContext, showAll, *path)
return health.doOnNext {
if (it.body.status == UP) {
logger.info("Health status: {}, {}", it.body.status, ObjectMapper().writeValueAsString(it.body))
}
}
}
}

Spring social siginIn() not firing on successful authentication

I've implemented Spring Social and have successfully been able to implement the ProviderSignInController to authenticate connections to Facebook.
As part of this I needed to implement the SignInAdapter interface. I understand this interface is used in the final steps of a successful authentication with a provider, specifically I'm overriding the signIn method to log the client into my application programmatically after successful authentication with the provider.
My problem is that when I implement the SignInAdapter it doesn't fire the signIn() method on successful login.
The code is almost straight out of the Spring showcase examples:
public class SimpleSignInAdapter implements SignInAdapter {
private static final Logger logger = LoggerFactory.getLogger(SimpleSignInAdapter.class);
private final RequestCache requestCache;
#Inject
public SimpleSignInAdapter(RequestCache requestCache) {
this.requestCache = requestCache;
logger.debug("Constructing " + SimpleSignInAdapter.class.getCanonicalName());
}
#Override
public String signIn(String localUserId, Connection<?> connection, NativeWebRequest request) {
/* A social profile has been found. Now we need to log that user into the
* application programatically.
*
* No other credentials are necessary here because by the time this method
* is called the user will have signed into the provider and their connection
* with that provider has been used to prove the user's identity.
*/
logger.debug("A social profile has been found. Now we need to log that user into the app.");
SignInUtils.signin(localUserId);
return null;
}
private String extractOriginalUrl(NativeWebRequest request) {
HttpServletRequest nativeReq = request.getNativeRequest(HttpServletRequest.class);
HttpServletResponse nativeRes = request.getNativeResponse(HttpServletResponse.class);
SavedRequest saved = requestCache.getRequest(nativeReq, nativeRes);
if (saved == null) {
return null;
}
requestCache.removeRequest(nativeReq, nativeRes);
removeAutheticationAttributes(nativeReq.getSession(false));
return saved.getRedirectUrl();
}
private void removeAutheticationAttributes(HttpSession session) {
if (session == null) {
return;
}
session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
}
}
The interesting this is that the SimpleSignInAdapter is constructed as I can debug through that step and see my debug output in the log so it seems the Bean is being instantiated just not firing the signIn method.
Here's my Spring configuration:
#Configuration
#ComponentScan(basePackages = "com.mycompany.webclient")
#PropertySource("classpath:app.properties")
#ImportResource("/WEB-INF/spring/appServlet/security-app-context.xml")
public class MainConfig {
#Bean
public PropertySourcesPlaceholderConfigurer propertyPlaceHolderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
}
And my Spring Social config is:
#Configuration
#EnableSocial
public class SocialConfig implements SocialConfigurer {
private SocialUserDAO socialUserDao;
//
// SocialConfigurer implementation methods
//
#Override
public void addConnectionFactories(ConnectionFactoryConfigurer cfConfig, Environment env) {
cfConfig.addConnectionFactory(new FacebookConnectionFactory("XXXXXXXXX", "XXXXXXXXX"));
cfConfig.addConnectionFactory(new TwitterConnectionFactory("XXXXXXXXX", "XXXXXXXXX"));
}
#Override
public UserIdSource getUserIdSource() {
return new UserIdSource() {
#Override
public String getUserId() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
throw new IllegalStateException("Unable to get a ConnectionRepository: no user signed in");
}
return authentication.getName();
}
};
}
#Override
public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
return new HibernateUsersConnectionRepository(socialUserDao, connectionFactoryLocator, Encryptors.noOpText());
}
//
// API Binding Beans
//
#Bean
#Scope(value="request", proxyMode=ScopedProxyMode.INTERFACES)
public Facebook facebook(ConnectionRepository repository) {
Connection<Facebook> connection = repository.findPrimaryConnection(Facebook.class);
return connection != null ? connection.getApi() : null;
}
#Bean
#Scope(value="request", proxyMode=ScopedProxyMode.INTERFACES)
public Twitter twitter(ConnectionRepository repository) {
Connection<Twitter> connection = repository.findPrimaryConnection(Twitter.class);
return connection != null ? connection.getApi() : null;
}
#Bean
#Scope(value="request", proxyMode=ScopedProxyMode.INTERFACES)
public LinkedIn linkedin(ConnectionRepository repository) {
Connection<LinkedIn> connection = repository.findPrimaryConnection(LinkedIn.class);
return connection != null ? connection.getApi() : null;
}
//
// Web Controller and Filter Beans
//
#Bean
public ConnectController connectController(ConnectionFactoryLocator connectionFactoryLocator, ConnectionRepository connectionRepository) {
ConnectController connectController = new ConnectController(connectionFactoryLocator, connectionRepository);
connectController.addInterceptor(new PostToWallAfterConnectInterceptor());
connectController.addInterceptor(new TweetAfterConnectInterceptor());
return connectController;
}
#Bean
public ProviderSignInController providerSignInController(ConnectionFactoryLocator connectionFactoryLocator, UsersConnectionRepository usersConnectionRepository) {
return new ProviderSignInController(connectionFactoryLocator, usersConnectionRepository, new SimpleSignInAdapter(new HttpSessionRequestCache()));
}
#Bean
public DisconnectController disconnectController(UsersConnectionRepository usersConnectionRepository, Environment env) {
return new DisconnectController(usersConnectionRepository, env.getProperty("facebook.clientSecret"));
}
#Bean
public ReconnectFilter apiExceptionHandler(UsersConnectionRepository usersConnectionRepository, UserIdSource userIdSource) {
return new ReconnectFilter(usersConnectionRepository, userIdSource);
}
}
Is there any way for me to confirm why the signIn() is not being fired or even if it is being registered.

Categories