SEBSERV-475 implemented
This commit is contained in:
parent
455d9809c3
commit
0d5d7b3894
6 changed files with 81 additions and 46 deletions
|
@ -131,7 +131,7 @@ public final class ClientConnection implements GrantEntity {
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
public final Long remoteProctoringRoomId;
|
public final Long remoteProctoringRoomId;
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
public final String virtualClientId;
|
public final String sebClientUserId;
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
public final Long creationTime;
|
public final Long creationTime;
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
|
@ -170,7 +170,7 @@ public final class ClientConnection implements GrantEntity {
|
||||||
this.userSessionId = userSessionId;
|
this.userSessionId = userSessionId;
|
||||||
this.info = info;
|
this.info = info;
|
||||||
this.vdi = false;
|
this.vdi = false;
|
||||||
this.virtualClientId = null;
|
this.sebClientUserId = null;
|
||||||
this.vdiPairToken = null;
|
this.vdiPairToken = null;
|
||||||
this.creationTime = 0L;
|
this.creationTime = 0L;
|
||||||
this.updateTime = 0L;
|
this.updateTime = 0L;
|
||||||
|
@ -199,7 +199,7 @@ public final class ClientConnection implements GrantEntity {
|
||||||
final String seb_os_name,
|
final String seb_os_name,
|
||||||
final String seb_machine_name,
|
final String seb_machine_name,
|
||||||
final String seb_version,
|
final String seb_version,
|
||||||
final String virtualClientId,
|
final String sebClientUserId,
|
||||||
final Boolean vdi,
|
final Boolean vdi,
|
||||||
final String vdiPairToken,
|
final String vdiPairToken,
|
||||||
final Long creationTime,
|
final Long creationTime,
|
||||||
|
@ -222,7 +222,7 @@ public final class ClientConnection implements GrantEntity {
|
||||||
this.sebOSName = seb_os_name;
|
this.sebOSName = seb_os_name;
|
||||||
this.sebMachineName = seb_machine_name;
|
this.sebMachineName = seb_machine_name;
|
||||||
this.sebVersion = seb_version;
|
this.sebVersion = seb_version;
|
||||||
this.virtualClientId = virtualClientId;
|
this.sebClientUserId = sebClientUserId;
|
||||||
this.vdi = vdi;
|
this.vdi = vdi;
|
||||||
this.vdiPairToken = vdiPairToken;
|
this.vdiPairToken = vdiPairToken;
|
||||||
this.creationTime = creationTime;
|
this.creationTime = creationTime;
|
||||||
|
@ -296,8 +296,8 @@ public final class ClientConnection implements GrantEntity {
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
public String getVirtualClientId() {
|
public String getSebClientUserId() {
|
||||||
return this.virtualClientId;
|
return this.sebClientUserId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnore // not used yet on GUI side
|
@JsonIgnore // not used yet on GUI side
|
||||||
|
@ -441,7 +441,7 @@ public final class ClientConnection implements GrantEntity {
|
||||||
builder.append(", remoteProctoringRoomId=");
|
builder.append(", remoteProctoringRoomId=");
|
||||||
builder.append(this.remoteProctoringRoomId);
|
builder.append(this.remoteProctoringRoomId);
|
||||||
builder.append(", virtualClientId=");
|
builder.append(", virtualClientId=");
|
||||||
builder.append(this.virtualClientId);
|
builder.append(this.sebClientUserId);
|
||||||
builder.append(", creationTime=");
|
builder.append(", creationTime=");
|
||||||
builder.append(this.creationTime);
|
builder.append(this.creationTime);
|
||||||
builder.append(", updateTime=");
|
builder.append(", updateTime=");
|
||||||
|
|
|
@ -366,7 +366,7 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
|
||||||
data.connectionToken,
|
data.connectionToken,
|
||||||
null,
|
null,
|
||||||
data.clientAddress,
|
data.clientAddress,
|
||||||
data.virtualClientId,
|
data.sebClientUserId,
|
||||||
BooleanUtils.toInteger(data.vdi, 1, 0, 0),
|
BooleanUtils.toInteger(data.vdi, 1, 0, 0),
|
||||||
data.vdiPairToken,
|
data.vdiPairToken,
|
||||||
millisecondsNow,
|
millisecondsNow,
|
||||||
|
@ -408,7 +408,7 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
|
||||||
null,
|
null,
|
||||||
data.userSessionId,
|
data.userSessionId,
|
||||||
data.clientAddress,
|
data.clientAddress,
|
||||||
data.virtualClientId,
|
data.sebClientUserId,
|
||||||
BooleanUtils.toInteger(data.vdi, 1, 0, 0),
|
BooleanUtils.toInteger(data.vdi, 1, 0, 0),
|
||||||
data.vdiPairToken,
|
data.vdiPairToken,
|
||||||
null,
|
null,
|
||||||
|
|
|
@ -84,7 +84,7 @@ public interface ExamSessionService {
|
||||||
Result<Collection<APIMessage>> checkExamConsistency(Long examId);
|
Result<Collection<APIMessage>> checkExamConsistency(Long examId);
|
||||||
|
|
||||||
/** Use this to check if a specified Exam has currently active SEB Client connections.
|
/** Use this to check if a specified Exam has currently active SEB Client connections.
|
||||||
*
|
* <p>
|
||||||
* Active SEB Client connections are established connections that are not yet closed and
|
* Active SEB Client connections are established connections that are not yet closed and
|
||||||
* open connection attempts.
|
* open connection attempts.
|
||||||
*
|
*
|
||||||
|
@ -163,7 +163,7 @@ public interface ExamSessionService {
|
||||||
OutputStream out);
|
OutputStream out);
|
||||||
|
|
||||||
/** Get current ClientConnectionData for a specified active SEB client connection.
|
/** Get current ClientConnectionData for a specified active SEB client connection.
|
||||||
*
|
* <p>
|
||||||
* active SEB client connections are connections that were initialized by a SEB client
|
* active SEB client connections are connections that were initialized by a SEB client
|
||||||
* on the particular server instance.
|
* on the particular server instance.
|
||||||
*
|
*
|
||||||
|
|
|
@ -57,7 +57,7 @@ public interface SEBClientSessionService extends ExamUpdateTask, SessionUpdateTa
|
||||||
/** Notify a SEB client event for live indication and storing to database.
|
/** Notify a SEB client event for live indication and storing to database.
|
||||||
*
|
*
|
||||||
* @param connectionToken the connection token
|
* @param connectionToken the connection token
|
||||||
* @param event The SEB client event data */
|
* @param jsonBody The SEB client event JSON data */
|
||||||
void notifyClientEvent(String connectionToken, String jsonBody);
|
void notifyClientEvent(String connectionToken, String jsonBody);
|
||||||
|
|
||||||
/** This is used to confirm SEB instructions that must be confirmed by the SEB client.
|
/** This is used to confirm SEB instructions that must be confirmed by the SEB client.
|
||||||
|
|
|
@ -10,11 +10,15 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl;
|
||||||
|
|
||||||
import java.security.Principal;
|
import java.security.Principal;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.Entity;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.util.Utils;
|
||||||
import org.apache.commons.lang3.BooleanUtils;
|
import org.apache.commons.lang3.BooleanUtils;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
@ -67,6 +71,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
||||||
private final ExamAdminService examAdminService;
|
private final ExamAdminService examAdminService;
|
||||||
private final DistributedIndicatorValueService distributedPingCache;
|
private final DistributedIndicatorValueService distributedPingCache;
|
||||||
private final SecurityKeyService securityKeyService;
|
private final SecurityKeyService securityKeyService;
|
||||||
|
private final SEBClientEventBatchService sebClientEventBatchService;
|
||||||
private final boolean isDistributedSetup;
|
private final boolean isDistributedSetup;
|
||||||
|
|
||||||
protected SEBClientConnectionServiceImpl(
|
protected SEBClientConnectionServiceImpl(
|
||||||
|
@ -76,7 +81,8 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
||||||
final DistributedIndicatorValueService distributedPingCache,
|
final DistributedIndicatorValueService distributedPingCache,
|
||||||
final ClientIndicatorFactory clientIndicatorFactory,
|
final ClientIndicatorFactory clientIndicatorFactory,
|
||||||
final SecurityKeyService securityKeyService,
|
final SecurityKeyService securityKeyService,
|
||||||
final WebserviceInfo webserviceInfo) {
|
final WebserviceInfo webserviceInfo,
|
||||||
|
final SEBClientEventBatchService sebClientEventBatchService) {
|
||||||
|
|
||||||
this.examSessionService = examSessionService;
|
this.examSessionService = examSessionService;
|
||||||
this.examSessionCacheService = examSessionService.getExamSessionCacheService();
|
this.examSessionCacheService = examSessionService.getExamSessionCacheService();
|
||||||
|
@ -87,6 +93,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
||||||
this.distributedPingCache = distributedPingCache;
|
this.distributedPingCache = distributedPingCache;
|
||||||
this.securityKeyService = securityKeyService;
|
this.securityKeyService = securityKeyService;
|
||||||
this.isDistributedSetup = webserviceInfo.isDistributed();
|
this.isDistributedSetup = webserviceInfo.isDistributed();
|
||||||
|
this.sebClientEventBatchService = sebClientEventBatchService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -113,7 +120,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
||||||
|
|
||||||
if (clientConfig == null) {
|
if (clientConfig == null) {
|
||||||
log.error("Illegal client connection request: requested connection config name: {}",
|
log.error("Illegal client connection request: requested connection config name: {}",
|
||||||
principal.getName());
|
(principal != null) ? principal.getName() : clientAddress);
|
||||||
throw new AccessDeniedException("Unknown or illegal client access");
|
throw new AccessDeniedException("Unknown or illegal client access");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -226,12 +233,12 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
||||||
if (StringUtils.isNoneBlank(clientAddress) &&
|
if (StringUtils.isNoneBlank(clientAddress) &&
|
||||||
StringUtils.isNotBlank(clientConnection.clientAddress) &&
|
StringUtils.isNotBlank(clientConnection.clientAddress) &&
|
||||||
!clientAddress.equals(clientConnection.clientAddress)) {
|
!clientAddress.equals(clientConnection.clientAddress)) {
|
||||||
|
// log SEB client IP address change
|
||||||
log.error(
|
log.error(
|
||||||
"ClientConnection integrity violation: client address mismatch: {}, {}",
|
"ClientConnection integrity violation: client address mismatch: {}, {}",
|
||||||
clientAddress,
|
clientAddress,
|
||||||
clientConnection.clientAddress);
|
clientConnection.clientAddress);
|
||||||
throw new IllegalArgumentException(
|
sebLogClientAddressMismatch(clientAddress, clientConnection);
|
||||||
"ClientConnection integrity violation: client address mismatch");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (examId != null) {
|
if (examId != null) {
|
||||||
|
@ -327,26 +334,26 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
||||||
|
|
||||||
ClientConnection clientConnection = getClientConnection(connectionToken);
|
ClientConnection clientConnection = getClientConnection(connectionToken);
|
||||||
|
|
||||||
// connection integrity check
|
// overall connection status integrity check
|
||||||
if (clientConnection.status == ConnectionStatus.ACTIVE) {
|
if (!clientConnection.status.clientActiveStatus) {
|
||||||
// connection already established. Check if IP is the same
|
|
||||||
if (StringUtils.isNoneBlank(clientAddress) &&
|
|
||||||
StringUtils.isNotBlank(clientConnection.clientAddress) &&
|
|
||||||
!clientAddress.equals(clientConnection.clientAddress)) {
|
|
||||||
log.warn(
|
|
||||||
"ClientConnection integrity violation: client address mismatch: {}, {}",
|
|
||||||
clientAddress,
|
|
||||||
clientConnection.clientAddress);
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"ClientConnection integrity violation: client address mismatch");
|
|
||||||
}
|
|
||||||
} else if (!clientConnection.status.clientActiveStatus) {
|
|
||||||
log.warn("ClientConnection integrity violation: client connection is not in expected state: {}",
|
log.warn("ClientConnection integrity violation: client connection is not in expected state: {}",
|
||||||
clientConnection);
|
clientConnection);
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"ClientConnection integrity violation: client connection is not in expected state");
|
"ClientConnection integrity violation: client connection is not in expected state");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check IP address change
|
||||||
|
if (StringUtils.isNoneBlank(clientAddress) &&
|
||||||
|
StringUtils.isNotBlank(clientConnection.clientAddress) &&
|
||||||
|
!clientAddress.equals(clientConnection.clientAddress)) {
|
||||||
|
// log client IP address change
|
||||||
|
log.warn(
|
||||||
|
"ClientConnection integrity violation: client address mismatch: {}, {}",
|
||||||
|
clientAddress,
|
||||||
|
clientConnection.clientAddress);
|
||||||
|
sebLogClientAddressMismatch(clientAddress, clientConnection);
|
||||||
|
}
|
||||||
|
|
||||||
if (log.isDebugEnabled()) {
|
if (log.isDebugEnabled()) {
|
||||||
log.debug(
|
log.debug(
|
||||||
"SEB client connection, establish ClientConnection for "
|
"SEB client connection, establish ClientConnection for "
|
||||||
|
@ -391,7 +398,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
||||||
final Long currentExamId = (examId != null) ? examId : clientConnection.examId;
|
final Long currentExamId = (examId != null) ? examId : clientConnection.examId;
|
||||||
final String currentVdiConnectionId = (clientId != null)
|
final String currentVdiConnectionId = (clientId != null)
|
||||||
? clientId
|
? clientId
|
||||||
: clientConnection.virtualClientId;
|
: clientConnection.sebClientUserId;
|
||||||
|
|
||||||
// create new ClientConnection for update
|
// create new ClientConnection for update
|
||||||
final ClientConnection establishedClientConnection = new ClientConnection(
|
final ClientConnection establishedClientConnection = new ClientConnection(
|
||||||
|
@ -438,12 +445,13 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
||||||
throw new IllegalStateException("ClientConnection integrity violation");
|
throw new IllegalStateException("ClientConnection integrity violation");
|
||||||
}
|
}
|
||||||
|
|
||||||
final ClientConnection connectionToSave = handleVDISetup(
|
// Removed this since VDI integration was postponed and has not been reactivated since then.
|
||||||
currentVdiConnectionId,
|
// final ClientConnection connectionToSave = handleVDISetup(
|
||||||
establishedClientConnection);
|
// currentVdiConnectionId,
|
||||||
|
// establishedClientConnection);
|
||||||
|
//
|
||||||
final ClientConnection updatedClientConnection = this.clientConnectionDAO
|
final ClientConnection updatedClientConnection = this.clientConnectionDAO
|
||||||
.save(connectionToSave)
|
.save(establishedClientConnection)
|
||||||
.getOrThrow();
|
.getOrThrow();
|
||||||
|
|
||||||
// check exam integrity for established connection
|
// check exam integrity for established connection
|
||||||
|
@ -467,13 +475,15 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
||||||
log.warn("Failed to load ClientConnectionDataInternal into cache on update");
|
log.warn("Failed to load ClientConnectionDataInternal into cache on update");
|
||||||
} else if (log.isDebugEnabled()) {
|
} else if (log.isDebugEnabled()) {
|
||||||
log.debug("SEB client connection, successfully established ClientConnection: {}",
|
log.debug("SEB client connection, successfully established ClientConnection: {}",
|
||||||
updatedClientConnection);
|
establishedClientConnection);
|
||||||
}
|
}
|
||||||
|
|
||||||
return updatedClientConnection;
|
return establishedClientConnection;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private ClientConnection handleVDISetup(
|
private ClientConnection handleVDISetup(
|
||||||
final String currentVdiConnectionId,
|
final String currentVdiConnectionId,
|
||||||
final ClientConnection establishedClientConnection) {
|
final ClientConnection establishedClientConnection) {
|
||||||
|
@ -485,7 +495,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
||||||
final Result<ClientConnectionRecord> vdiPairConnectionResult =
|
final Result<ClientConnectionRecord> vdiPairConnectionResult =
|
||||||
this.clientConnectionDAO.getVDIPairCompanion(
|
this.clientConnectionDAO.getVDIPairCompanion(
|
||||||
establishedClientConnection.examId,
|
establishedClientConnection.examId,
|
||||||
establishedClientConnection.virtualClientId);
|
establishedClientConnection.sebClientUserId);
|
||||||
|
|
||||||
if (!vdiPairConnectionResult.hasValue()) {
|
if (!vdiPairConnectionResult.hasValue()) {
|
||||||
return establishedClientConnection;
|
return establishedClientConnection;
|
||||||
|
@ -506,7 +516,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
establishedClientConnection.virtualClientId,
|
establishedClientConnection.sebClientUserId,
|
||||||
null,
|
null,
|
||||||
vdiPairCompanion.getConnectionToken(),
|
vdiPairCompanion.getConnectionToken(),
|
||||||
null,
|
null,
|
||||||
|
@ -554,7 +564,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
||||||
.byConnectionToken(connectionToken)
|
.byConnectionToken(connectionToken)
|
||||||
.getOrThrow();
|
.getOrThrow();
|
||||||
|
|
||||||
ClientConnection updatedClientConnection;
|
final ClientConnection updatedClientConnection;
|
||||||
if (clientConnection.status != ConnectionStatus.CLOSED) {
|
if (clientConnection.status != ConnectionStatus.CLOSED) {
|
||||||
updatedClientConnection = saveInState(
|
updatedClientConnection = saveInState(
|
||||||
clientConnection,
|
clientConnection,
|
||||||
|
@ -614,7 +624,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
||||||
.byConnectionToken(connectionToken)
|
.byConnectionToken(connectionToken)
|
||||||
.getOrThrow();
|
.getOrThrow();
|
||||||
|
|
||||||
ClientConnection updatedClientConnection;
|
final ClientConnection updatedClientConnection;
|
||||||
if (DISABLE_STATE_PREDICATE.test(clientConnection)) {
|
if (DISABLE_STATE_PREDICATE.test(clientConnection)) {
|
||||||
|
|
||||||
updatedClientConnection = saveInState(
|
updatedClientConnection = saveInState(
|
||||||
|
@ -658,12 +668,37 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
||||||
.map(token -> disableConnection(token, institutionId)
|
.map(token -> disableConnection(token, institutionId)
|
||||||
.onError(error -> log.error("Failed to disable SEB client connection: {}", token))
|
.onError(error -> log.error("Failed to disable SEB client connection: {}", token))
|
||||||
.getOr(null))
|
.getOr(null))
|
||||||
.filter(clientConnection -> clientConnection != null)
|
.filter(Objects::nonNull)
|
||||||
.map(clientConnection -> clientConnection.getEntityKey())
|
.map(Entity::getEntityKey)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SEBSERV-475 IP address change during handshake is possible but is logged within SEB logs
|
||||||
|
private void sebLogClientAddressMismatch(
|
||||||
|
final String clientAddress,
|
||||||
|
final ClientConnection clientConnection) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
final long now = Utils.getMillisecondsNow();
|
||||||
|
this.sebClientEventBatchService.accept(new SEBClientEventBatchService.EventData(
|
||||||
|
clientConnection.connectionToken,
|
||||||
|
now,
|
||||||
|
new ClientEvent(
|
||||||
|
null,
|
||||||
|
clientConnection.id,
|
||||||
|
ClientEvent.EventType.WARN_LOG,
|
||||||
|
now, now, null,
|
||||||
|
"SEB Client IP address changed: " +
|
||||||
|
clientConnection.clientAddress +
|
||||||
|
" -> " +
|
||||||
|
clientAddress
|
||||||
|
)));
|
||||||
|
} catch (final Exception e) {
|
||||||
|
log.error("Failed to log SEB client IP address change: ", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void checkExamRunning(final Long examId, final String user, final String address) {
|
private void checkExamRunning(final Long examId, final String user, final String address) {
|
||||||
if (examId != null && !this.examSessionService.isExamRunning(examId)) {
|
if (examId != null && !this.examSessionService.isExamRunning(examId)) {
|
||||||
examNotRunningException(examId, user, address);
|
examNotRunningException(examId, user, address);
|
||||||
|
@ -730,7 +765,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// try to get user account display name
|
// try to get user account display name (SEBSERV-228)
|
||||||
String accountId = userSessionId;
|
String accountId = userSessionId;
|
||||||
try {
|
try {
|
||||||
final String newAccountId = this.examSessionService
|
final String newAccountId = this.examSessionService
|
||||||
|
|
|
@ -176,7 +176,7 @@ public class SEBClientEventBatchService {
|
||||||
log.debug("SEBClientEventBatchService worker {} processes batch of size {} in {} ms",
|
log.debug("SEBClientEventBatchService worker {} processes batch of size {} in {} ms",
|
||||||
workerName,
|
workerName,
|
||||||
size,
|
size,
|
||||||
start - Utils.getMillisecondsNow());
|
Utils.getMillisecondsNow() - start);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
|
|
Loading…
Reference in a new issue