PDF Cert validation (#2394)
* verifyCerts * cert info * Hardening suggestions for Stirling-PDF / certValidate (#2395) * Protect `readLine()` against DoS * Switch order of literals to prevent NullPointerException --------- Co-authored-by: pixeebot[bot] <104101892+pixeebot[bot]@users.noreply.github.com> * some basic html excaping and translation fixing --------- Co-authored-by: pixeebot[bot] <104101892+pixeebot[bot]@users.noreply.github.com> Co-authored-by: a <a>
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
package stirling.software.SPDF.config.security;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.*;
|
||||
|
||||
@@ -13,7 +12,6 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.DependsOn;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.ProviderManager;
|
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
@@ -32,35 +30,21 @@ import org.springframework.security.oauth2.client.registration.InMemoryClientReg
|
||||
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
|
||||
import org.springframework.security.saml2.core.Saml2X509Credential;
|
||||
import org.springframework.security.saml2.core.Saml2X509Credential.Saml2X509CredentialType;
|
||||
import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest;
|
||||
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
|
||||
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
||||
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
|
||||
import org.springframework.security.saml2.provider.service.web.HttpSessionSaml2AuthenticationRequestRepository;
|
||||
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
|
||||
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
|
||||
import org.springframework.security.web.context.SecurityContextHolderFilter;
|
||||
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
|
||||
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
|
||||
import org.springframework.security.web.savedrequest.NullRequestCache;
|
||||
import org.springframework.security.web.session.ForceEagerSessionCreationFilter;
|
||||
import org.springframework.security.web.session.HttpSessionEventPublisher;
|
||||
import org.springframework.security.web.session.SessionManagementFilter;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.session.web.http.CookieSerializer;
|
||||
import org.springframework.session.web.http.DefaultCookieSerializer;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler;
|
||||
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
|
||||
@@ -163,7 +147,7 @@ public class SecurityConfiguration {
|
||||
http.sessionManagement(
|
||||
sessionManagement ->
|
||||
sessionManagement
|
||||
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
|
||||
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
|
||||
.maximumSessions(10)
|
||||
.maxSessionsPreventsLogin(false)
|
||||
.sessionRegistry(sessionRegistry)
|
||||
@@ -287,7 +271,6 @@ public class SecurityConfiguration {
|
||||
relyingPartyRegistrations())
|
||||
.authenticationManager(
|
||||
new ProviderManager(authenticationProvider))
|
||||
|
||||
.successHandler(
|
||||
new CustomSaml2AuthenticationSuccessHandler(
|
||||
loginAttemptService,
|
||||
@@ -452,7 +435,7 @@ public class SecurityConfiguration {
|
||||
.clientName("OIDC")
|
||||
.build());
|
||||
}
|
||||
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(
|
||||
name = "security.saml2.enabled",
|
||||
@@ -506,7 +489,7 @@ public class SecurityConfiguration {
|
||||
|
||||
AuthnRequest authnRequest = customizer.getAuthnRequest();
|
||||
log.debug("AuthnRequest ID: {}", authnRequest.getID());
|
||||
|
||||
|
||||
if (authnRequest.getID() == null) {
|
||||
authnRequest.setID("ARQ" + UUID.randomUUID().toString());
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ public class StampController {
|
||||
return WebResponseUtils.pdfDocToWebResponse(
|
||||
document,
|
||||
Filenames.toSimpleFileName(pdfFile.getOriginalFilename())
|
||||
.replaceFirst("[.][^.]+$", "")
|
||||
.replaceFirst("[.][^.]+$", "")
|
||||
+ "_stamped.pdf");
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ public class StampController {
|
||||
String fileExtension = resourceDir.substring(resourceDir.lastIndexOf("."));
|
||||
File tempFile = Files.createTempFile("NotoSansFont", fileExtension).toFile();
|
||||
try (InputStream is = classPathResource.getInputStream();
|
||||
FileOutputStream os = new FileOutputStream(tempFile)) {
|
||||
FileOutputStream os = new FileOutputStream(tempFile)) {
|
||||
IOUtils.copy(is, os);
|
||||
font = PDType0Font.load(document, tempFile);
|
||||
} finally {
|
||||
@@ -339,4 +339,4 @@ public class StampController {
|
||||
private float calculateTextCapHeight(PDFont font, float fontSize) {
|
||||
return font.getFontDescriptor().getCapHeight() / 1000 * fontSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
package stirling.software.SPDF.controller.api.security;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
|
||||
import org.bouncycastle.cert.X509CertificateHolder;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||
import org.bouncycastle.cms.CMSProcessable;
|
||||
import org.bouncycastle.cms.CMSProcessableByteArray;
|
||||
import org.bouncycastle.cms.CMSSignedData;
|
||||
import org.bouncycastle.cms.SignerInformation;
|
||||
import org.bouncycastle.cms.SignerInformationStore;
|
||||
import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
|
||||
import org.bouncycastle.util.Store;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.api.security.SignatureValidationRequest;
|
||||
import stirling.software.SPDF.model.api.security.SignatureValidationResult;
|
||||
import stirling.software.SPDF.service.CertificateValidationService;
|
||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/security")
|
||||
@Tag(name = "Security", description = "Security APIs")
|
||||
public class ValidateSignatureController {
|
||||
|
||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||
private final CertificateValidationService certValidationService;
|
||||
|
||||
@Autowired
|
||||
public ValidateSignatureController(
|
||||
CustomPDDocumentFactory pdfDocumentFactory,
|
||||
CertificateValidationService certValidationService) {
|
||||
this.pdfDocumentFactory = pdfDocumentFactory;
|
||||
this.certValidationService = certValidationService;
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Validate PDF Digital Signature",
|
||||
description =
|
||||
"Validates the digital signatures in a PDF file against default or custom certificates. Input:PDF Output:JSON Type:SISO")
|
||||
@PostMapping(value = "/validate-signature")
|
||||
public ResponseEntity<List<SignatureValidationResult>> validateSignature(
|
||||
@ModelAttribute SignatureValidationRequest request) throws IOException {
|
||||
List<SignatureValidationResult> results = new ArrayList<>();
|
||||
MultipartFile file = request.getFileInput();
|
||||
|
||||
// Load custom certificate if provided
|
||||
X509Certificate customCert = null;
|
||||
if (request.getCertFile() != null && !request.getCertFile().isEmpty()) {
|
||||
try (ByteArrayInputStream certStream =
|
||||
new ByteArrayInputStream(request.getCertFile().getBytes())) {
|
||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||
customCert = (X509Certificate) cf.generateCertificate(certStream);
|
||||
} catch (CertificateException e) {
|
||||
throw new RuntimeException("Invalid certificate file: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
try (PDDocument document = pdfDocumentFactory.load(file.getInputStream())) {
|
||||
List<PDSignature> signatures = document.getSignatureDictionaries();
|
||||
|
||||
for (PDSignature sig : signatures) {
|
||||
SignatureValidationResult result = new SignatureValidationResult();
|
||||
|
||||
try {
|
||||
byte[] signedContent = sig.getSignedContent(file.getInputStream());
|
||||
byte[] signatureBytes = sig.getContents(file.getInputStream());
|
||||
|
||||
CMSProcessable content = new CMSProcessableByteArray(signedContent);
|
||||
CMSSignedData signedData = new CMSSignedData(content, signatureBytes);
|
||||
|
||||
Store<X509CertificateHolder> certStore = signedData.getCertificates();
|
||||
SignerInformationStore signerStore = signedData.getSignerInfos();
|
||||
|
||||
for (SignerInformation signer : signerStore.getSigners()) {
|
||||
X509CertificateHolder certHolder = (X509CertificateHolder) certStore.getMatches(signer.getSID()).iterator().next();
|
||||
X509Certificate cert = new JcaX509CertificateConverter().getCertificate(certHolder);
|
||||
|
||||
boolean isValid = signer.verify(new JcaSimpleSignerInfoVerifierBuilder().build(cert));
|
||||
result.setValid(isValid);
|
||||
|
||||
// Additional validations
|
||||
result.setChainValid(customCert != null
|
||||
? certValidationService.validateCertificateChainWithCustomCert(cert, customCert)
|
||||
: certValidationService.validateCertificateChain(cert));
|
||||
|
||||
result.setTrustValid(customCert != null
|
||||
? certValidationService.validateTrustWithCustomCert(cert, customCert)
|
||||
: certValidationService.validateTrustStore(cert));
|
||||
|
||||
result.setNotRevoked(!certValidationService.isRevoked(cert));
|
||||
result.setNotExpired(!cert.getNotAfter().before(new Date()));
|
||||
|
||||
// Set basic signature info
|
||||
result.setSignerName(sig.getName());
|
||||
result.setSignatureDate(sig.getSignDate().getTime().toString());
|
||||
result.setReason(sig.getReason());
|
||||
result.setLocation(sig.getLocation());
|
||||
|
||||
// Set new certificate details
|
||||
result.setIssuerDN(cert.getIssuerX500Principal().getName());
|
||||
result.setSubjectDN(cert.getSubjectX500Principal().getName());
|
||||
result.setSerialNumber(cert.getSerialNumber().toString(16)); // Hex format
|
||||
result.setValidFrom(cert.getNotBefore().toString());
|
||||
result.setValidUntil(cert.getNotAfter().toString());
|
||||
result.setSignatureAlgorithm(cert.getSigAlgName());
|
||||
|
||||
// Get key size (if possible)
|
||||
try {
|
||||
result.setKeySize(((RSAPublicKey) cert.getPublicKey()).getModulus().bitLength());
|
||||
} catch (Exception e) {
|
||||
// If not RSA or error, set to 0
|
||||
result.setKeySize(0);
|
||||
}
|
||||
|
||||
result.setVersion(String.valueOf(cert.getVersion()));
|
||||
|
||||
// Set key usage
|
||||
List<String> keyUsages = new ArrayList<>();
|
||||
boolean[] keyUsageFlags = cert.getKeyUsage();
|
||||
if (keyUsageFlags != null) {
|
||||
String[] keyUsageLabels = {
|
||||
"Digital Signature", "Non-Repudiation", "Key Encipherment",
|
||||
"Data Encipherment", "Key Agreement", "Certificate Signing",
|
||||
"CRL Signing", "Encipher Only", "Decipher Only"
|
||||
};
|
||||
for (int i = 0; i < keyUsageFlags.length; i++) {
|
||||
if (keyUsageFlags[i]) {
|
||||
keyUsages.add(keyUsageLabels[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
result.setKeyUsages(keyUsages);
|
||||
|
||||
// Check if self-signed
|
||||
result.setSelfSigned(cert.getSubjectX500Principal().equals(cert.getIssuerX500Principal()));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
result.setValid(false);
|
||||
result.setErrorMessage("Signature validation failed: " + e.getMessage());
|
||||
}
|
||||
|
||||
results.add(result);
|
||||
}
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(results);
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,13 @@ public class SecurityWebController {
|
||||
return "security/cert-sign";
|
||||
}
|
||||
|
||||
@GetMapping("/validate-signature")
|
||||
@Hidden
|
||||
public String certSignVerifyForm(Model model) {
|
||||
model.addAttribute("currentPage", "validate-signature");
|
||||
return "security/validate-signature";
|
||||
}
|
||||
|
||||
@GetMapping("/remove-cert-sign")
|
||||
@Hidden
|
||||
public String certUnSignForm(Model model) {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package stirling.software.SPDF.model.api.security;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import stirling.software.SPDF.model.api.PDFFile;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class SignatureValidationRequest extends PDFFile {
|
||||
|
||||
@Schema(description = "(Optional) file to compare PDF cert signatures against x.509 format")
|
||||
private MultipartFile certFile;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package stirling.software.SPDF.model.api.security;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class SignatureValidationResult {
|
||||
private boolean valid;
|
||||
private String signerName;
|
||||
private String signatureDate;
|
||||
private String reason;
|
||||
private String location;
|
||||
private String errorMessage;
|
||||
private boolean chainValid;
|
||||
private boolean trustValid;
|
||||
private boolean notExpired;
|
||||
private boolean notRevoked;
|
||||
|
||||
private String issuerDN; // Certificate issuer's Distinguished Name
|
||||
private String subjectDN; // Certificate subject's Distinguished Name
|
||||
private String serialNumber; // Certificate serial number
|
||||
private String validFrom; // Certificate validity start date
|
||||
private String validUntil; // Certificate validity end date
|
||||
private String signatureAlgorithm;// Algorithm used for signing
|
||||
private int keySize; // Key size in bits
|
||||
private String version; // Certificate version
|
||||
private List<String> keyUsages; // List of key usage purposes
|
||||
private boolean isSelfSigned; // Whether the certificate is self-signed
|
||||
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package stirling.software.SPDF.service;
|
||||
|
||||
import io.github.pixee.security.BoundedLineReader;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.cert.CertPath;
|
||||
import java.security.cert.CertPathValidator;
|
||||
import java.security.cert.CertificateExpiredException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.CertificateNotYetValidException;
|
||||
import java.security.cert.PKIXParameters;
|
||||
import java.security.cert.TrustAnchor;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Arrays;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
|
||||
@Service
|
||||
public class CertificateValidationService {
|
||||
private KeyStore trustStore;
|
||||
|
||||
@PostConstruct
|
||||
private void initializeTrustStore() throws Exception {
|
||||
trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
trustStore.load(null, null);
|
||||
loadMozillaCertificates();
|
||||
}
|
||||
|
||||
private void loadMozillaCertificates() throws Exception {
|
||||
try (InputStream is = getClass().getResourceAsStream("/certdata.txt")) {
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
|
||||
String line;
|
||||
StringBuilder certData = new StringBuilder();
|
||||
boolean inCert = false;
|
||||
int certCount = 0;
|
||||
|
||||
while ((line = BoundedLineReader.readLine(reader, 5_000_000)) != null) {
|
||||
if (line.startsWith("CKA_VALUE MULTILINE_OCTAL")) {
|
||||
inCert = true;
|
||||
certData = new StringBuilder();
|
||||
continue;
|
||||
}
|
||||
if (inCert) {
|
||||
if ("END".equals(line)) {
|
||||
inCert = false;
|
||||
byte[] certBytes = parseOctalData(certData.toString());
|
||||
if (certBytes != null) {
|
||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||
X509Certificate cert =
|
||||
(X509Certificate)
|
||||
cf.generateCertificate(
|
||||
new ByteArrayInputStream(certBytes));
|
||||
trustStore.setCertificateEntry("mozilla-cert-" + certCount++, cert);
|
||||
}
|
||||
} else {
|
||||
certData.append(line).append("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] parseOctalData(String data) {
|
||||
try {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
String[] tokens = data.split("\\\\");
|
||||
for (String token : tokens) {
|
||||
token = token.trim();
|
||||
if (!token.isEmpty()) {
|
||||
baos.write(Integer.parseInt(token, 8));
|
||||
}
|
||||
}
|
||||
return baos.toByteArray();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean validateCertificateChain(X509Certificate cert) {
|
||||
try {
|
||||
CertPathValidator validator = CertPathValidator.getInstance("PKIX");
|
||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||
List<X509Certificate> certList = Arrays.asList(cert);
|
||||
CertPath certPath = cf.generateCertPath(certList);
|
||||
|
||||
Set<TrustAnchor> anchors = new HashSet<>();
|
||||
Enumeration<String> aliases = trustStore.aliases();
|
||||
while (aliases.hasMoreElements()) {
|
||||
Object trustCert = trustStore.getCertificate(aliases.nextElement());
|
||||
if (trustCert instanceof X509Certificate) {
|
||||
anchors.add(new TrustAnchor((X509Certificate) trustCert, null));
|
||||
}
|
||||
}
|
||||
|
||||
PKIXParameters params = new PKIXParameters(anchors);
|
||||
params.setRevocationEnabled(false);
|
||||
validator.validate(certPath, params);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean validateTrustStore(X509Certificate cert) {
|
||||
try {
|
||||
Enumeration<String> aliases = trustStore.aliases();
|
||||
while (aliases.hasMoreElements()) {
|
||||
Object trustCert = trustStore.getCertificate(aliases.nextElement());
|
||||
if (trustCert instanceof X509Certificate && cert.equals(trustCert)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch (KeyStoreException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isRevoked(X509Certificate cert) {
|
||||
try {
|
||||
cert.checkValidity();
|
||||
return false;
|
||||
} catch (CertificateExpiredException | CertificateNotYetValidException e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean validateCertificateChainWithCustomCert(
|
||||
X509Certificate cert, X509Certificate customCert) {
|
||||
try {
|
||||
cert.verify(customCert.getPublicKey());
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean validateTrustWithCustomCert(X509Certificate cert, X509Certificate customCert) {
|
||||
try {
|
||||
// Compare the issuer of the signature certificate with the custom certificate
|
||||
return cert.getIssuerX500Principal().equals(customCert.getSubjectX500Principal());
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user