Sebastian
Sebastian

Reputation: 71

Spring websocket is automatically closed after 30 minutes (timeout)

I'm trying to implement a websocket on using Spring Boot (1.5.13).

The messaging works fine but after about 30 minutes the connection is terminated by the server (Reason 1008 - "This connection was established under an authenticated HTTP session that has ended"). I've tried to set different timeouts but doesn't seem to have any effect.

@Service
@RequiredArgsConstructor
@Slf4j
public class OCPPSocketHandler extends TextWebSocketHandler {
    @Override
    public void handleTextMessage(WebSocketSession webSocketSession, TextMessage textMessage)
        throws IOException {
      ...
    }
}

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    public static final String ENDPOINT = "/pp/v2.0";

    @Autowired
    private CustomSocketHandler socketHandler;

    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(
            new CustomExceptionWebSocketHandlerDecorator(socketHandler), ENDPOINT
        )
        .setAllowedOrigins("*");
    }
}

application.properties:

#6h as milliseconds
server.connection-timeout=3600000 
server.servlet.session.timeout=6h

A TextMessage (WebSocket) is sent every 30 minutes to keep the connection alive.

I've seen this question about session timeouts but I can't see a solution there

Upvotes: 7

Views: 19299

Answers (4)

Shuibin Long
Shuibin Long

Reputation: 11

From the Docs in 4.2.4. Server Configuration is the following, hope it can help you:

Each underlying WebSocket engine exposes configuration properties that control runtime characteristics, such as the size of message buffer sizes, idle timeout, and others.

For Tomcat, WildFly, and GlassFish, you can add a ServletServerContainerFactoryBean to your WebSocket Java config, as the following example shows:

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Bean
    public ServletServerContainerFactoryBean createWebSocketContainer() {

        ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
        container.setMaxSessionIdleTimeout(120000L); // set the timeout to 2min

        // ...
        return container;
    }

}

For Jetty, you need to supply a pre-configured Jetty WebSocketServerFactory and plug that into Spring’s DefaultHandshakeHandler through your WebSocket Java config. The following example shows how to do so:

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(echoWebSocketHandler(),
            "/echo").setHandshakeHandler(handshakeHandler());
    }

    @Bean
    public DefaultHandshakeHandler handshakeHandler() {

        WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER);
        policy.setInputBufferSize(8192);
        policy.setIdleTimeout(600000); // set the timeout to 10min

        return new DefaultHandshakeHandler(
                new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy)));
    }

}

Upvotes: 1

chan lee
chan lee

Reputation: 1

  1. In my case, timeout of HttpSession in SpringBoot is set to server.servlet.session.timeout: 1 (1 minute)

  2. Set websocket endpoint in Spring Security config to httpBasic

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.antMatcher("/ocpp/**")
        .authorizeRequests()                         
        .anyRequest().permitAll()//.hasAnyRole("CS","SYSTEM_ADMIN")
        .and().httpBasic();
    }
    
  3. use basic authentication in websocket client

        String id = "testuser";
        String pw = "1234";
        String idpw = new String(Base64.getEncoder().encode((id+":"+pw).getBytes()));
    
        System.out.println("id["+id+"]pw["+pw+"]idpw["+idpw+"]");
        ;
        Map<String, String> httpHeaders = new HashMap<String, String>();
        httpHeaders.put("authorization", "Basic "+idpw);
        WebSocketClient webSocketClient = new WebSocketClient(new URI("ws://localhost:9112/ocpp/testuser")
                , new Draft_6455(Collections.emptyList(), Collections.singletonList(new Protocol("ocpp2.0.1")))
                , httpHeaders
        ) {
        ...
    

In this condition, if the endpoint is a protected resource, websocket close 1008 occurs when the HttpSession is terminated.

Reference: Read Paragraph 7.2

**The solution is to directly implement http basic processing in websocket addInterceptor. (Spring Security httpBasic not use) **

add addInterceptor in WebSocketConfig.java:

.addInterceptors(new HandshakeInterceptor() {
    @Override
    public boolean beforeHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
        log.debug("===============================================================");
        log.debug("beforeHandshake[]"+attributes);
        ServletServerHttpRequest ssreq = (ServletServerHttpRequest)serverHttpRequest;
        ServletServerHttpResponse ssres = (ServletServerHttpResponse)serverHttpResponse;
        HttpServletRequest req = ssreq.getServletRequest();
        HttpServletResponse res = ssres.getServletResponse();
        HttpSession session = req.getSession();
        log.debug("session["+session.getId());
        log.debug("session["+session.getMaxInactiveInterval());

        //authentication
        try {
            String header = req.getHeader("Authorization");
            log.debug("header[" + header + "]");
            if (header == null) {
                log.debug("The Authorization header is empty");
//                                    throw new BadCredentialsException("The Authorization header is empty");
            } else {
                header = header.trim();
                if (!StringUtils.startsWithIgnoreCase(header, "Basic")) {
                    log.debug("The Authorization header does not start with Basic.");
                } else if (header.equalsIgnoreCase("Basic")) {
                    throw new BadCredentialsException("Empty basic authentication token");
                } else {
                    byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8);
                    byte[] decoded;
                    try {
                        decoded = Base64.getDecoder().decode(base64Token);
                    } catch (IllegalArgumentException var8) {
                        throw new BadCredentialsException("Failed to decode basic authentication token");
                    }
                    String token = new String(decoded, "UTF-8");
                    int delim = token.indexOf(":");
                    if (delim == -1) {
                        throw new BadCredentialsException("Invalid basic authentication token");
                    } else {
                        log.info("TOKEN [" +token+"]");
                        String principal = token.substring(0, delim);
                        String credencial = token.substring(delim + 1);
                        //your 
                        
                        if(principal.equals("testuser") && credencial.equals("1234")){
                            log.debug("login OK");
                        }else{
                            throw new BadCredentialsException("Invalid basic authentication token");
                        }
                    }
                }
            }
        }catch(Exception e){
            log.error("Basic Authentication error", e);

            res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            res.addHeader("WWW-Authenticate", "Basic realm=" + "Realm" + "");
            PrintWriter pw = res.getWriter();
            pw.println("Invalid status code received: 401 Status line: HTTP/1.1 401");
            return false;
        }

        return true;
    }

Upvotes: 0

debugging
debugging

Reputation: 61

I found my WebSocket closed after 30 minutes too.

The root reason is the http session will close after 30 minutes by default in SpringBoot.

In SpringBoot config property server.servlet.session.timeout will change the default behavior, but there might have some limit.

Besides, WebSocket connections have pingpong messages to keep alive, so the connection should never be closed, until the pingpong stops.

After some tracing, I found a way to solve this problem:

  1. The timer to close the connection is here io.undertow.server.session.InMemorySessionManager.SessionImpl in my case.
  2. As we can see io.undertow.server.session.InMemorySessionManager.SessionImpl#setMaxInactiveInterval will reset the timer.
  3. This method will be called by javax.servlet.http.HttpSession#setMaxInactiveInterval.
  4. So, once setMaxInactiveInterval is called before each timeout, the connection will never be closed.

Here is my implementation:

  1. Store the HandshakeRequest into the javax.websocket.EndpointConfig#getUserProperties in the Configurator javax.websocket.server.ServerEndpointConfig.Configurator#modifyHandshake
  2. Our clients will send String messages to keep alive, so in the onMessage method, fetch the HandshakeRequest from javax.websocket.Session#getUserProperties, then
HttpSession httpSession = (HttpSession) handshakeRequest.getHttpSession();
httpSession.setMaxInactiveInterval((int) (session.getMaxIdleTimeout() / 1000));

That's all, hope it helps.

Upvotes: 4

Davide Lorenzo MARINO
Davide Lorenzo MARINO

Reputation: 26926

The connection can be closed if any between server or client closes it, and also by a firewall if it notes that there is no activity on the established connection.

So you need to check timeout also on the client side. It is reasanable that 30 minutes is the default timeout for this kind of connection so it seams that on the client side it uses default values.

Additionally it is a good design to check periodically the status of the connection, for example sending a sort of ping (message from the client) /pong (response from the server) message. If you do it every minute for example you have an idea of the connection status and the connection will never be closed for inactivity.

Upvotes: 0

Related Questions