|
|
|
@ -18,14 +18,19 @@ package org.thingsboard.server.controller.plugin; |
|
|
|
import lombok.extern.slf4j.Slf4j; |
|
|
|
import org.springframework.beans.factory.BeanCreationNotAllowedException; |
|
|
|
import org.springframework.beans.factory.annotation.Autowired; |
|
|
|
import org.springframework.context.annotation.Lazy; |
|
|
|
import org.springframework.beans.factory.annotation.Value; |
|
|
|
import org.springframework.scheduling.annotation.Scheduled; |
|
|
|
import org.springframework.stereotype.Service; |
|
|
|
import org.springframework.web.socket.CloseStatus; |
|
|
|
import org.springframework.web.socket.TextMessage; |
|
|
|
import org.springframework.web.socket.WebSocketSession; |
|
|
|
import org.springframework.web.socket.handler.TextWebSocketHandler; |
|
|
|
import org.thingsboard.server.common.data.id.CustomerId; |
|
|
|
import org.thingsboard.server.common.data.id.TenantId; |
|
|
|
import org.thingsboard.server.common.data.id.UserId; |
|
|
|
import org.thingsboard.server.config.WebSocketConfiguration; |
|
|
|
import org.thingsboard.server.service.security.model.SecurityUser; |
|
|
|
import org.thingsboard.server.service.security.model.UserPrincipal; |
|
|
|
import org.thingsboard.server.service.telemetry.SessionEvent; |
|
|
|
import org.thingsboard.server.service.telemetry.TelemetryWebSocketMsgEndpoint; |
|
|
|
import org.thingsboard.server.service.telemetry.TelemetryWebSocketService; |
|
|
|
@ -34,6 +39,7 @@ import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef; |
|
|
|
import java.io.IOException; |
|
|
|
import java.net.URI; |
|
|
|
import java.security.InvalidParameterException; |
|
|
|
import java.util.Set; |
|
|
|
import java.util.UUID; |
|
|
|
import java.util.concurrent.ConcurrentHashMap; |
|
|
|
import java.util.concurrent.ConcurrentMap; |
|
|
|
@ -48,12 +54,26 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr |
|
|
|
@Autowired |
|
|
|
private TelemetryWebSocketService webSocketService; |
|
|
|
|
|
|
|
@Value("${server.ws.limits.max_sessions_per_tenant:0}") |
|
|
|
private int maxSessionsPerTenant; |
|
|
|
@Value("${server.ws.limits.max_sessions_per_customer:0}") |
|
|
|
private int maxSessionsPerCustomer; |
|
|
|
@Value("${server.ws.limits.max_sessions_per_regular_user:0}") |
|
|
|
private int maxSessionsPerRegularUser; |
|
|
|
@Value("${server.ws.limits.max_sessions_per_public_user:0}") |
|
|
|
private int maxSessionsPerPublicUser; |
|
|
|
|
|
|
|
private ConcurrentMap<TenantId, Set<String>> tenantSessionsMap = new ConcurrentHashMap<>(); |
|
|
|
private ConcurrentMap<CustomerId, Set<String>> customerSessionsMap = new ConcurrentHashMap<>(); |
|
|
|
private ConcurrentMap<UserId, Set<String>> regularUserSessionsMap = new ConcurrentHashMap<>(); |
|
|
|
private ConcurrentMap<UserId, Set<String>> publicUserSessionsMap = new ConcurrentHashMap<>(); |
|
|
|
|
|
|
|
@Override |
|
|
|
public void handleTextMessage(WebSocketSession session, TextMessage message) { |
|
|
|
try { |
|
|
|
SessionMetaData sessionMd = internalSessionMap.get(session.getId()); |
|
|
|
if (sessionMd != null) { |
|
|
|
log.info("[{}][{}] Processing {}", sessionMd.sessionRef.getSecurityCtx().getTenantId(), session.getId(), message); |
|
|
|
log.info("[{}][{}] Processing {}", sessionMd.sessionRef.getSecurityCtx().getTenantId(), session.getId(), message.getPayload()); |
|
|
|
webSocketService.handleWebSocketMsg(sessionMd.sessionRef, message.getPayload()); |
|
|
|
} else { |
|
|
|
log.warn("[{}] Failed to find session", session.getId()); |
|
|
|
@ -71,12 +91,15 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr |
|
|
|
String internalSessionId = session.getId(); |
|
|
|
TelemetryWebSocketSessionRef sessionRef = toRef(session); |
|
|
|
String externalSessionId = sessionRef.getSessionId(); |
|
|
|
if (!checkLimits(session, sessionRef)) { |
|
|
|
return; |
|
|
|
} |
|
|
|
internalSessionMap.put(internalSessionId, new SessionMetaData(session, sessionRef)); |
|
|
|
externalSessionMap.put(externalSessionId, internalSessionId); |
|
|
|
processInWebSocketService(sessionRef, SessionEvent.onEstablished()); |
|
|
|
log.info("[{}][{}][{}] Session is opened", sessionRef.getSecurityCtx().getTenantId(), externalSessionId, session.getId()); |
|
|
|
} catch (InvalidParameterException e) { |
|
|
|
log.warn("[[{}] Failed to start session", session.getId(), e); |
|
|
|
log.warn("[{}] Failed to start session", session.getId(), e); |
|
|
|
session.close(CloseStatus.BAD_DATA.withReason(e.getMessage())); |
|
|
|
} catch (Exception e) { |
|
|
|
log.warn("[{}] Failed to start session", session.getId(), e); |
|
|
|
@ -101,6 +124,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr |
|
|
|
super.afterConnectionClosed(session, closeStatus); |
|
|
|
SessionMetaData sessionMd = internalSessionMap.remove(session.getId()); |
|
|
|
if (sessionMd != null) { |
|
|
|
cleanupLimits(session, sessionMd.sessionRef); |
|
|
|
externalSessionMap.remove(sessionMd.sessionRef.getSessionId()); |
|
|
|
processInWebSocketService(sessionMd.sessionRef, SessionEvent.onClosed()); |
|
|
|
} |
|
|
|
@ -136,7 +160,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr |
|
|
|
private final WebSocketSession session; |
|
|
|
private final TelemetryWebSocketSessionRef sessionRef; |
|
|
|
|
|
|
|
public SessionMetaData(WebSocketSession session, TelemetryWebSocketSessionRef sessionRef) { |
|
|
|
SessionMetaData(WebSocketSession session, TelemetryWebSocketSessionRef sessionRef) { |
|
|
|
super(); |
|
|
|
this.session = session; |
|
|
|
this.sessionRef = sessionRef; |
|
|
|
@ -162,15 +186,21 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@Override |
|
|
|
public void close(TelemetryWebSocketSessionRef sessionRef) throws IOException { |
|
|
|
close(sessionRef, CloseStatus.NORMAL); |
|
|
|
} |
|
|
|
|
|
|
|
@Override |
|
|
|
public void close(TelemetryWebSocketSessionRef sessionRef, CloseStatus reason) throws IOException { |
|
|
|
String externalId = sessionRef.getSessionId(); |
|
|
|
log.debug("[{}] Processing close request", externalId); |
|
|
|
String internalId = externalSessionMap.get(externalId); |
|
|
|
if (internalId != null) { |
|
|
|
SessionMetaData sessionMd = internalSessionMap.get(internalId); |
|
|
|
if (sessionMd != null) { |
|
|
|
sessionMd.session.close(CloseStatus.NORMAL); |
|
|
|
sessionMd.session.close(reason); |
|
|
|
} else { |
|
|
|
log.warn("[{}][{}] Failed to find session by internal id", externalId, internalId); |
|
|
|
} |
|
|
|
@ -179,4 +209,94 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private boolean checkLimits(WebSocketSession session, TelemetryWebSocketSessionRef sessionRef) throws Exception { |
|
|
|
String sessionId = session.getId(); |
|
|
|
if (maxSessionsPerTenant > 0) { |
|
|
|
Set<String> tenantSessions = tenantSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getTenantId(), id -> ConcurrentHashMap.newKeySet()); |
|
|
|
synchronized (tenantSessions) { |
|
|
|
if (tenantSessions.size() < maxSessionsPerTenant) { |
|
|
|
tenantSessions.add(sessionId); |
|
|
|
} else { |
|
|
|
log.info("[{}][{}][{}] Failed to start session. Max tenant sessions limit reached" |
|
|
|
, sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), sessionId); |
|
|
|
session.close(CloseStatus.POLICY_VIOLATION.withReason("Max tenant sessions limit reached!")); |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (sessionRef.getSecurityCtx().isCustomerUser()) { |
|
|
|
if (maxSessionsPerCustomer > 0) { |
|
|
|
Set<String> customerSessions = customerSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getCustomerId(), id -> ConcurrentHashMap.newKeySet()); |
|
|
|
synchronized (customerSessions) { |
|
|
|
if (customerSessions.size() < maxSessionsPerCustomer) { |
|
|
|
customerSessions.add(sessionId); |
|
|
|
} else { |
|
|
|
log.info("[{}][{}][{}] Failed to start session. Max customer sessions limit reached" |
|
|
|
, sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), sessionId); |
|
|
|
session.close(CloseStatus.POLICY_VIOLATION.withReason("Max customer sessions limit reached")); |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
if (maxSessionsPerRegularUser > 0 && UserPrincipal.Type.USER_NAME.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) { |
|
|
|
Set<String> regularUserSessions = regularUserSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet()); |
|
|
|
synchronized (regularUserSessions) { |
|
|
|
if (regularUserSessions.size() < maxSessionsPerRegularUser) { |
|
|
|
regularUserSessions.add(sessionId); |
|
|
|
} else { |
|
|
|
log.info("[{}][{}][{}] Failed to start session. Max user sessions limit reached" |
|
|
|
, sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), sessionId); |
|
|
|
session.close(CloseStatus.POLICY_VIOLATION.withReason("Max regular user sessions limit reached")); |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
if (maxSessionsPerPublicUser > 0 && UserPrincipal.Type.PUBLIC_ID.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) { |
|
|
|
Set<String> publicUserSessions = publicUserSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet()); |
|
|
|
synchronized (publicUserSessions) { |
|
|
|
if (publicUserSessions.size() < maxSessionsPerPublicUser) { |
|
|
|
publicUserSessions.add(sessionId); |
|
|
|
} else { |
|
|
|
log.info("[{}][{}][{}] Failed to start session. Max user sessions limit reached" |
|
|
|
, sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), sessionId); |
|
|
|
session.close(CloseStatus.POLICY_VIOLATION.withReason("Max public user sessions limit reached")); |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
private void cleanupLimits(WebSocketSession session, TelemetryWebSocketSessionRef sessionRef) { |
|
|
|
String sessionId = session.getId(); |
|
|
|
if (maxSessionsPerTenant > 0) { |
|
|
|
Set<String> tenantSessions = tenantSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getTenantId(), id -> ConcurrentHashMap.newKeySet()); |
|
|
|
synchronized (tenantSessions) { |
|
|
|
tenantSessions.remove(sessionId); |
|
|
|
} |
|
|
|
} |
|
|
|
if (sessionRef.getSecurityCtx().isCustomerUser()) { |
|
|
|
if (maxSessionsPerCustomer > 0) { |
|
|
|
Set<String> customerSessions = customerSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getCustomerId(), id -> ConcurrentHashMap.newKeySet()); |
|
|
|
synchronized (customerSessions) { |
|
|
|
customerSessions.remove(sessionId); |
|
|
|
} |
|
|
|
} |
|
|
|
if (maxSessionsPerRegularUser > 0 && UserPrincipal.Type.USER_NAME.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) { |
|
|
|
Set<String> regularUserSessions = regularUserSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet()); |
|
|
|
synchronized (regularUserSessions) { |
|
|
|
regularUserSessions.remove(sessionId); |
|
|
|
} |
|
|
|
} |
|
|
|
if (maxSessionsPerPublicUser > 0 && UserPrincipal.Type.PUBLIC_ID.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) { |
|
|
|
Set<String> publicUserSessions = publicUserSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet()); |
|
|
|
synchronized (publicUserSessions) { |
|
|
|
publicUserSessions.remove(sessionId); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|